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

@ -589,6 +589,99 @@ public class UiButtonTests
return CreateButton(info);
}
// ── 2026-08-17 morning gate finding 3: the PassToChildren cascade +
// per-state P0x3B (Invisible) honor. Fixture mirrors the live map
// town-hotspot template (0x100001F0 in 0x21000026, raw-DAT-dumped):
// a media-less RolloverEnabled button whose Normal/Normal_rollover
// descriptors are PassToChildren and whose highlight child authors
// Normal={0x3B:true} / Normal_rollover={0x3B:false}. ──
private static ElementInfo MapNoteShapedInfo(bool passToChildren = true)
{
var info = new ElementInfo { Type = 1, Width = 10, Height = 10 };
AddBoolProperty(info, 0x13u, true); // P0x13 RolloverEnabled
info.States[UiButtonStateMachine.Normal] = new UiStateInfo
{
Id = UiButtonStateMachine.Normal,
Name = "Normal",
PassToChildren = passToChildren,
};
info.States[UiButtonStateMachine.NormalRollover] = new UiStateInfo
{
Id = UiButtonStateMachine.NormalRollover,
Name = "Normal_rollover",
PassToChildren = passToChildren,
};
return info;
}
private static UiDatElement HighlightChild()
{
var info = new ElementInfo { Type = 3, Width = 10, Height = 10 };
var normal = new UiStateInfo { Id = 1, Name = "Normal" };
normal.Properties.Values[0x3Bu] = new UiPropertyValue
{ Kind = UiPropertyKind.Bool, BoolValue = true };
var rollover = new UiStateInfo { Id = 2, Name = "Normal_rollover" };
rollover.Properties.Values[0x3Bu] = new UiPropertyValue
{ Kind = UiPropertyKind.Bool, BoolValue = false };
info.States[1] = normal;
info.States[2] = rollover;
return new UiDatElement(info, NoTex);
}
[Fact]
public void PassToChildrenStates_CascadeToStatefulChildren_DrivingPerStateInvisible()
{
// Retail UIElement::SetState @0x00464E70's PassToChildren cascade +
// OnSetAttribute @0x00462d80 case 8 (P0x3B -> SetVisible(value==0)):
// the map marker's green rollover frame is hidden at rest and shown
// only while the pointer is over the button.
var b = CreateButton(MapNoteShapedInfo());
UiDatElement kid = HighlightChild();
b.AddChild(kid);
Assert.True(kid.Visible); // pre-cascade construction default
// The initial Normal application (retail's own initial UpdateState_)
// hides the highlight.
Assert.True(b.TrySetRetailState(UiButtonStateMachine.Normal));
Assert.False(kid.Visible);
b.OnEvent(new UiEvent(0, b, UiEventType.HoverEnter));
Assert.True(kid.Visible); // Normal_rollover: P0x3B=false -> shown
b.OnEvent(new UiEvent(0, b, UiEventType.HoverLeave));
Assert.False(kid.Visible); // back to Normal: P0x3B=true -> hidden
}
[Fact]
public void StatesWithoutPassToChildren_DoNotCascade()
{
var b = CreateButton(MapNoteShapedInfo(passToChildren: false));
UiDatElement kid = HighlightChild();
b.AddChild(kid);
b.TrySetRetailState(UiButtonStateMachine.Normal);
b.OnEvent(new UiEvent(0, b, UiEventType.HoverEnter));
Assert.True(kid.Visible); // never cascaded, never hidden/shown
}
[Fact]
public void UiDatElement_TrySetRetailState_HonorsPerStateInvisible()
{
// The child half in isolation — the state-transition application of
// dat property 0x3B, distinct from ElementReader.Invisible's
// construction-time effective read (GF-13/AP-230).
UiDatElement kid = HighlightChild();
Assert.True(kid.Visible);
Assert.True(kid.TrySetRetailState(1u));
Assert.False(kid.Visible);
Assert.True(kid.TrySetRetailState(2u));
Assert.True(kid.Visible);
}
private static ElementInfo ButtonInfo(params string[] states)
{
var info = new ElementInfo { Type = 1, Width = 20, Height = 20 };