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:
Erik 2026-08-17 09:22:46 +02:00
parent ef567bfa20
commit 942a02af11
12 changed files with 682 additions and 46 deletions

View file

@ -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()