fix(ui): morning gate — map town markers: green rollover highlight + the authored map-note tooltip skin/font
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>
This commit is contained in:
parent
ef567bfa20
commit
942a02af11
12 changed files with 682 additions and 46 deletions
|
|
@ -66,7 +66,15 @@ public sealed class MapPageController
|
|||
// ElementInfos only exist inside the full panel-slot resolve tree —
|
||||
// a cold ImportInfos(hostLayout, iconId) re-import CANNOT find them
|
||||
// (register row AD-108's live-DAT finding; see Bind's own doc).
|
||||
Func<ElementInfo, UiElement?> IconBuilder);
|
||||
Func<ElementInfo, UiElement?> IconBuilder,
|
||||
// The template's resolved ElementInfo (production: the SAME cached
|
||||
// RowTemplateResolver the TemplateResolver above builds from —
|
||||
// RowTemplateResolver.ResolveInfo). BuildTownMarkers needs the
|
||||
// AUTHORED shape the built UiButton widget does not carry: the
|
||||
// button-swallowed rollover-highlight child subtree (2026-08-17
|
||||
// morning gate finding 3). Null (fixtures) = markers get no
|
||||
// highlight child, everything else unchanged.
|
||||
Func<uint, uint, ElementInfo?>? TemplateInfoResolver = null);
|
||||
|
||||
private readonly UiElement? _dateTimeText;
|
||||
private readonly UiElement? _map;
|
||||
|
|
@ -259,6 +267,28 @@ public sealed class MapPageController
|
|||
/// referenced LayoutDesc/element) leaves the map usable without
|
||||
/// hotspots rather than failing the whole page — matches retail's own
|
||||
/// null-guarded <c>if (eax_10 != 0)</c> before the loop.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>2026-08-17 morning gate finding 3 (live-DAT re-derivation,
|
||||
/// <c>MapHousePanelSlotProbeTests</c>):</b> the hotspot template
|
||||
/// (<c>0x100001F0</c> in <c>0x21000026</c>) is NOT "a plain 10x10
|
||||
/// hotspot dot" — its DirectState authors the full retail note
|
||||
/// behavior: its OWN tooltip-popup locator
|
||||
/// <c>P0x47=0x10000398</c>/<c>P0x48=0x21000041</c> (the map-note popup
|
||||
/// skin, whose incorporated text child <c>0x10000396</c> fonts
|
||||
/// <c>0x40000015</c> — the other three skins font <c>0x40000002</c>),
|
||||
/// a zero per-element tooltip delay (<c>P0x50=0.0</c> — town tooltips
|
||||
/// fire the instant the dwell arms), <c>P0x4B</c> TooltipOn, and
|
||||
/// <c>P0x13</c> RolloverEnabled. Its <c>Normal</c>/<c>Normal_rollover</c>
|
||||
/// states are PassToChildren descriptors driving the highlight child
|
||||
/// <c>0x100001F1</c> (base <c>0x100002B7</c>@<c>0x21000042</c> — a
|
||||
/// four-piece frame of pure-green <c>0x06004CC9</c>, byte-decoded
|
||||
/// A=FF R=00 G=FF B=00) via per-state <c>P0x3B</c>: hidden at rest,
|
||||
/// GREEN on rollover. The earlier hardcoded shared-skin override here
|
||||
/// was therefore wrong on both counts and is removed — the built
|
||||
/// marker's own authored locator/delay (populated by
|
||||
/// <see cref="LayoutImporter"/> from the template's property bag) wins.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void BuildTownMarkers(ElementInfo? mapInfo, Func<uint, uint, UiElement?> templateResolver)
|
||||
{
|
||||
|
|
@ -267,6 +297,12 @@ public sealed class MapPageController
|
|||
if (!mapInfo.TryGetEffectiveProperty(HotspotTemplateLayoutAttr, out var templateLayout)) return;
|
||||
if (templateLayout.UnsignedValue == 0) return;
|
||||
|
||||
// The template's AUTHORED shape — for the button-swallowed
|
||||
// highlight child (UiButton.ConsumesDatChildren drops it from the
|
||||
// built widget, same class of loss as the AD-108 icons).
|
||||
ElementInfo? templateInfo = _bindings.TemplateInfoResolver?.Invoke(
|
||||
(uint)templateLayout.UnsignedValue, (uint)templateElement.UnsignedValue);
|
||||
|
||||
foreach (MapLocation loc in MapLocations.All)
|
||||
{
|
||||
UiElement? marker = templateResolver(
|
||||
|
|
@ -284,31 +320,39 @@ public sealed class MapPageController
|
|||
// backing UiElement.GetTooltipText()'s override, which
|
||||
// RetailTooltipPresenter.ResolveTooltipText consults BEFORE the
|
||||
// authored text (closes register row TS-85's last item,
|
||||
// gmMapUI::AddMapNote @0x004A1C51). AuthoredTooltipRootElementId/
|
||||
// LayoutDid still gate the popup SKIN unconditionally even on
|
||||
// the runtime-text path — the map-note template (0x100001F0)
|
||||
// authors no individual tooltip-popup locator of its own (a
|
||||
// plain 10x10 hotspot dot), so RetailTooltipPresenter's popup
|
||||
// needs one supplied; RetailTooltipPresenter.SharedPopupSkinRootElementId/
|
||||
// SharedPopupSkinLayoutDid (see that class's own single
|
||||
// canonical citation, night-round review F10) is the same
|
||||
// proven-working skin UiItemSlot already hardcodes.
|
||||
// gmMapUI::AddMapNote @0x004A1C51). The popup SKIN + delay come
|
||||
// from the marker's own authored properties (see this method's
|
||||
// doc — finding 3 removed the shared-skin override that
|
||||
// previously clobbered them here).
|
||||
if (marker is UiButton markerButton)
|
||||
markerButton.TooltipText = loc.Name;
|
||||
else
|
||||
// F11 (night-round review): silently skipping the runtime-
|
||||
// text write here would leave the marker's popup mounted
|
||||
// (AuthoredTooltipRootElementId/LayoutDid are still set
|
||||
// below) but genuinely EMPTY — a live-DAT template change
|
||||
// that resolves 0x100001F0 to something other than a
|
||||
// UiButton would regress every town-marker tooltip with no
|
||||
// text write here would leave the marker's popup authored
|
||||
// but genuinely EMPTY — a live-DAT template change that
|
||||
// resolves 0x100001F0 to something other than a UiButton
|
||||
// would regress every town-marker tooltip with no
|
||||
// diagnostic signal at all.
|
||||
Console.WriteLine(
|
||||
$"[D.2b] Map tab: town marker '{loc.Name}' template "
|
||||
+ $"resolved to {marker.GetType().Name}, not UiButton — "
|
||||
+ "TooltipText cannot be set, marker will show no tooltip.");
|
||||
marker.AuthoredTooltipRootElementId = RetailTooltipPresenter.SharedPopupSkinRootElementId;
|
||||
marker.AuthoredTooltipLayoutDid = RetailTooltipPresenter.SharedPopupSkinLayoutDid;
|
||||
|
||||
// AddMapNote's CreateChildElement builds the note's FULL
|
||||
// authored subtree; our UiButton swallows dat children, so the
|
||||
// rollover-highlight child (and its green-frame grandchildren)
|
||||
// is rebuilt per marker through the SAME IconBuilder seam the
|
||||
// AD-108 icons use, then re-armed by applying the button's
|
||||
// initial Normal state (retail UIElement_Button's own initial
|
||||
// UpdateState_ -> SetState(Normal) -> PassToChildren cascade ->
|
||||
// the child's Normal-state P0x3B=true hides it until rollover).
|
||||
if (templateInfo is not null && marker is UiButton stateHost)
|
||||
{
|
||||
foreach (ElementInfo childInfo in templateInfo.Children)
|
||||
if (_bindings.IconBuilder(childInfo) is { } highlight)
|
||||
marker.AddChild(highlight);
|
||||
stateHost.TrySetRetailState(UiButtonStateMachine.Normal);
|
||||
}
|
||||
_map!.AddChild(marker);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -369,13 +369,18 @@ public sealed class RetailTooltipPresenter : IDisposable
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Night-round review F10: previously duplicated as three separate
|
||||
/// private constants with three separate partial citations — this
|
||||
/// class's own world-hover popup (below), <c>UiItemSlot</c>'s item-cell
|
||||
/// popup, and <c>MapPageController</c>'s town-marker popup. All three
|
||||
/// consumers now reference these SAME two constants; this is the ONE
|
||||
/// citation. This class is the natural owner since it's the mount
|
||||
/// point every one of the three consumers ultimately routes through
|
||||
/// (<c>OnTooltipShow</c>/<c>UpdateWorldHoverTooltip</c> both call
|
||||
/// private constants with three separate partial citations; this is the
|
||||
/// ONE citation. Two consumers remain — this class's own world-hover
|
||||
/// popup (below) and <c>UiItemSlot</c>'s item-cell popup.
|
||||
/// <c>MapPageController</c>'s town markers were REMOVED from the list at
|
||||
/// the 2026-08-17 morning gate round (finding 3): the hotspot template
|
||||
/// (<c>0x100001F0</c> in <c>0x21000026</c>) turned out to author its OWN
|
||||
/// popup locator (<c>P0x47=0x10000398</c>/<c>P0x48=0x21000041</c> — the
|
||||
/// fourth skin, whose incorporated text child fonts <c>0x40000015</c>
|
||||
/// where the other three font <c>0x40000002</c>), which the earlier
|
||||
/// shared-constant override was clobbering. This class is the natural
|
||||
/// owner since it's the mount point every consumer ultimately routes
|
||||
/// through (<c>OnTooltipShow</c>/<c>UpdateWorldHoverTooltip</c> both call
|
||||
/// <c>TryBuildAndMountPopup</c> with these values or a widget's own).
|
||||
/// </remarks>
|
||||
public const uint SharedPopupSkinRootElementId = 0x10000395u;
|
||||
|
|
|
|||
|
|
@ -71,4 +71,23 @@ public sealed class RowTemplateResolver
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,36 @@ public class UiDatElement : UiElement, IUiDatStateful
|
|||
ActiveState = stateName;
|
||||
}
|
||||
|
||||
// Per-state Invisible (dat property 0x3B): retail's SetState applies
|
||||
// the committed state's properties through UIElement::OnSetAttribute,
|
||||
// whose case 8 (@0x00462d80, property id 0x33 + 8 = 0x3B) is
|
||||
// `SetVisible(value == 0)` — an authored per-STATE true hides the
|
||||
// element for that state. First consumer (2026-08-17 morning gate
|
||||
// finding 3): the map town-hotspot template's highlight child
|
||||
// (0x100001F1 in 0x21000026, base 0x100002B7@0x21000042 — the green
|
||||
// 0x06004CC9 frame) authors Normal={0x3B:true} /
|
||||
// Normal_rollover={0x3B:false}, i.e. hidden at rest, shown on
|
||||
// rollover.
|
||||
//
|
||||
// SCOPED TO NAMED STATES ONLY: a 0x3B authored in the unnamed
|
||||
// DirectState is the CONSTRUCTION-time "authored invisible" class
|
||||
// (1,083 elements client-wide — docs/ISSUES.md #408, its own
|
||||
// separately-gated general-honor item; ElementReader.Invisible/GF-13
|
||||
// captures it and only chargen's scoped walk acts on it, register
|
||||
// AP-230). Honoring it here would un-gate #408 through the back
|
||||
// door: LayoutImporter.BuildWidget's post-children state reapply
|
||||
// calls TrySetRetailState(DirectStateId) on every built widget, so
|
||||
// a DirectState honor would hide all 1,083 at import (measured
|
||||
// same-round: 10 combat-layout elements incl. 0x10000454 went
|
||||
// un-hit-testable, breaking the spell-favorite drag tests). The
|
||||
// NAMED-state flip below is a live visibility state machine that
|
||||
// cannot work at all without the honor — that is this port's line.
|
||||
if (stateId != UiStateInfo.DirectStateId
|
||||
&& selectedState is not null
|
||||
&& selectedState.Properties.TryGetValue(0x3Bu, out var invisibleProp)
|
||||
&& invisibleProp.Kind == UiPropertyKind.Bool)
|
||||
Visible = !invisibleProp.BoolValue;
|
||||
|
||||
if (selectedState?.PassToChildren == true)
|
||||
{
|
||||
foreach (UiElement child in Children)
|
||||
|
|
|
|||
|
|
@ -3328,6 +3328,15 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
lock (_bindings.Assets.DatLock)
|
||||
return hotspotTemplate.Resolve(templateLayoutId, templateElementId);
|
||||
}
|
||||
// The import half alone (same cache): BuildTownMarkers reads the
|
||||
// template's AUTHORED child subtree — the button-swallowed rollover
|
||||
// highlight (morning gate finding 3) — which the built widget above
|
||||
// does not carry.
|
||||
ElementInfo? ResolveHotspotTemplateInfo(uint templateLayoutId, uint templateElementId)
|
||||
{
|
||||
lock (_bindings.Assets.DatLock)
|
||||
return hotspotTemplate.ResolveInfo(templateLayoutId, templateElementId);
|
||||
}
|
||||
|
||||
// m_pMap's two button-swallowed icon children (player/house markers)
|
||||
// only exist as ElementInfos INSIDE rootInfo's own panel-slot resolve
|
||||
|
|
@ -3356,7 +3365,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
PlayerCellId: mh.PlayerCellId,
|
||||
HousePosition: mh.HousePosition ?? (static () => null),
|
||||
TemplateResolver: ResolveHotspotTemplate,
|
||||
IconBuilder: BuildSwallowedIcon),
|
||||
IconBuilder: BuildSwallowedIcon,
|
||||
TemplateInfoResolver: ResolveHotspotTemplateInfo),
|
||||
House: new Layout.HousePageController.Bindings(
|
||||
Lines: mh.HouseLines ?? (static () => Array.Empty<string>()),
|
||||
OnShown: mh.HouseShown,
|
||||
|
|
|
|||
|
|
@ -398,11 +398,13 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
if (!HasStateMedia(""))
|
||||
return false;
|
||||
ActiveState = "";
|
||||
CascadeStateToChildren(stateId);
|
||||
return true;
|
||||
}
|
||||
if (TryFindState(stateId, out var state))
|
||||
{
|
||||
ActiveState = state.Name;
|
||||
CascadeStateToChildren(stateId);
|
||||
return true;
|
||||
}
|
||||
string stateName = UiButtonStateMachine.StateName(stateId);
|
||||
|
|
@ -411,6 +413,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
if (!string.IsNullOrEmpty(stateName) && HasStateMedia(stateName))
|
||||
{
|
||||
ActiveState = stateName;
|
||||
CascadeStateToChildren(stateId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -993,6 +996,40 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
// segments carry no Highlight art), so ActiveState stays "Normal"
|
||||
// forever, but the spin's OWN label color must still swap.
|
||||
ApplyPerStateLabelStyle(requested);
|
||||
|
||||
// Same unconditional-commit principle for the child cascade: retail
|
||||
// UIElement::SetState @0x00464E70 propagates the committed state to
|
||||
// children whenever the StateDesc authors PassToChildren, regardless
|
||||
// of whether THIS element's own sprite changed — keyed off the
|
||||
// REQUESTED id for the same reason as the label style above.
|
||||
CascadeStateToChildren(requested);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>UIElement::SetState @0x00464E70</c>'s PassToChildren
|
||||
/// cascade, ported for buttons (2026-08-17 morning gate finding 3 —
|
||||
/// previously only <see cref="Layout.UiDatElement.TrySetRetailState"/>
|
||||
/// had it). First consumer: the map town-hotspot template
|
||||
/// (<c>0x100001F0</c> in <c>0x21000026</c>) authors media-less
|
||||
/// <c>Normal</c>/<c>Normal_rollover</c> state descriptors with
|
||||
/// <c>PassToChildren=true</c> whose only job is driving the highlight
|
||||
/// child's per-state <c>P0x3B</c> visibility flip (the green
|
||||
/// <c>0x06004CC9</c> rollover frame). No-op unless a state descriptor
|
||||
/// for <paramref name="stateId"/> exists AND authors PassToChildren AND
|
||||
/// this button actually has stateful children — dat-built buttons
|
||||
/// normally have none (<see cref="ConsumesDatChildren"/>); only
|
||||
/// explicitly-attached subtrees (the map markers' highlight, the
|
||||
/// AD-108 icon seam) can receive the cascade.
|
||||
/// </summary>
|
||||
private void CascadeStateToChildren(uint stateId)
|
||||
{
|
||||
if (Children.Count == 0)
|
||||
return;
|
||||
if (!TryFindState(stateId, out UiStateInfo state) || !state.PassToChildren)
|
||||
return;
|
||||
foreach (UiElement child in Children)
|
||||
if (child is IUiDatStateful stateful)
|
||||
stateful.TrySetRetailState(stateId);
|
||||
}
|
||||
|
||||
private uint ComputeRequestedStateId()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue