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

@ -128,11 +128,133 @@ public sealed class MapHousePanelSlotProbeTests
if (hasTemplateElement && hasTemplateLayoutDid && tl.UnsignedValue != 0)
{
ElementInfo? template = LayoutImporter.ImportInfos(dats, (uint)tl.UnsignedValue, (uint)te.UnsignedValue);
Console.WriteLine(template is null
? "[maphouse] hotspot template IMPORT NULL"
: $"[maphouse] hotspot template type={template.Type} "
+ $"({template.Width}x{template.Height}) states={template.States.Count} "
+ $"stateMedia={template.StateMedia.Count}");
if (template is null)
{
Console.WriteLine("[maphouse] hotspot template IMPORT NULL");
}
else
{
// 2026-08-17 morning gate finding 3: the note element's
// tooltip popup locator is the TEMPLATE's own authored
// P0x47/P0x48 (StartTooltipAtMouse reads them off the
// created note element; P0x48-absent falls back to the
// note's own source layout = m_pMap's P0x48), and the
// hover highlight is the template's own state machine.
// Dump all of it.
string tp47 = template.TryGetEffectiveProperty(0x47u, out var tpe)
? $"0x{tpe.UnsignedValue:X8} (kind={tpe.Kind})" : "ABSENT";
string tp48 = template.TryGetEffectiveProperty(0x48u, out var tpl)
? $"0x{tpl.UnsignedValue:X8} (kind={tpl.Kind})" : "ABSENT";
string tp4b = template.TryGetEffectiveProperty(0x4Bu, out var tpb)
? tpb.UnsignedValue.ToString() : "ABSENT";
Console.WriteLine(
$"[maphouse] hotspot template 0x{template.Id:X8} type={template.Type} "
+ $"({template.Width}x{template.Height}) states={template.States.Count} "
+ $"stateMedia={template.StateMedia.Count} defaultState='{template.DefaultStateName}' "
+ $"popupRoot(P0x47)={tp47} popupLayout(P0x48)={tp48} tooltipOn(P0x4B)={tp4b} "
+ $"fontDid=0x{template.FontDid:X8} kids={template.Children.Count}");
foreach (var s in template.States)
{
Console.WriteLine(
$"[maphouse] template state id=0x{s.Key:X} name='{s.Value.Name}' "
+ $"props={s.Value.Properties.Values.Count} "
+ $"image={(s.Value.Image is { } img ? $"0x{img.File:X8}/{img.DrawMode}" : "none")}");
foreach (var pv in s.Value.Properties.Values)
Console.WriteLine(
$"[maphouse] t-prop 0x{pv.Key:X} kind={pv.Value.Kind} "
+ $"u=0x{pv.Value.UnsignedValue:X} i={pv.Value.IntegerValue} b={pv.Value.BoolValue} "
+ $"color={pv.Value.ColorValue}");
}
foreach (var m in template.StateMedia)
Console.WriteLine(
$"[maphouse] template stateMedia '{m.Key}' file=0x{m.Value.File:X8} drawMode={m.Value.DrawMode}");
foreach (ElementInfo tc in template.Children)
{
Console.WriteLine(
$"[maphouse] template kid 0x{tc.Id:X8} type={tc.Type} "
+ $"({tc.X},{tc.Y} {tc.Width}x{tc.Height}) stateMedia={tc.StateMedia.Count} "
+ $"states={tc.States.Count} defaultState='{tc.DefaultStateName}' kids={tc.Children.Count} "
+ $"origParent={(tc.HasOriginalParentSize ? $"{tc.OriginalParentWidth}x{tc.OriginalParentHeight}" : "none")} "
+ $"edges=({tc.Left},{tc.Top},{tc.Right},{tc.Bottom})");
foreach (ElementInfo gk in tc.Children)
Console.WriteLine(
$"[maphouse] kid-kid 0x{gk.Id:X8} type={gk.Type} "
+ $"({gk.X},{gk.Y} {gk.Width}x{gk.Height}) stateMedia={gk.StateMedia.Count} "
+ $"origParent={(gk.HasOriginalParentSize ? $"{gk.OriginalParentWidth}x{gk.OriginalParentHeight}" : "none")} "
+ $"edges=({gk.Left},{gk.Top},{gk.Right},{gk.Bottom})");
foreach (var m in tc.StateMedia)
Console.WriteLine(
$"[maphouse] kid stateMedia '{m.Key}' file=0x{m.Value.File:X8} drawMode={m.Value.DrawMode}");
foreach (var s in tc.States)
{
Console.WriteLine(
$"[maphouse] kid state id=0x{s.Key:X} name='{s.Value.Name}' "
+ $"props={s.Value.Properties.Values.Count} "
+ $"image={(s.Value.Image is { } kimg ? $"0x{kimg.File:X8}/{kimg.DrawMode}" : "none")}");
foreach (var pv in s.Value.Properties.Values)
Console.WriteLine(
$"[maphouse] prop 0x{pv.Key:X} kind={pv.Value.Kind} "
+ $"u=0x{pv.Value.UnsignedValue:X} i={pv.Value.IntegerValue} f={pv.Value.FloatValue} "
+ $"b={pv.Value.BoolValue} color={pv.Value.ColorValue}");
}
}
// Comparison sweep: all four popup skins in 0x21000041 —
// root media + the INCORPORATED text child 0x10000396's
// per-parent effective font (child-table merge can
// override per skin; the finding-3 question is whether
// the map-note skin's text child fonts differently from
// the generic 0x10000395's).
foreach (uint skinId in new[] { 0x10000487u, 0x10000395u, 0x10000397u, 0x10000398u })
{
ElementInfo? s41 = LayoutImporter.ImportInfos(dats, 0x21000041u, skinId);
if (s41 is null)
{
Console.WriteLine($"[maphouse] skin-sweep 0x{skinId:X8} IMPORT NULL");
continue;
}
string rootMedia = s41.StateMedia.TryGetValue("", out var rm)
? $"0x{rm.File:X8}/{rm.DrawMode}" : "none";
ElementInfo? text = null;
foreach (ElementInfo k in s41.Children)
if (k.Id == s41.TooltipTextChildElementId) { text = k; break; }
Console.WriteLine(
$"[maphouse] skin-sweep 0x{skinId:X8} ({s41.Width}x{s41.Height}) rootMedia={rootMedia} "
+ $"textChild=0x{s41.TooltipTextChildElementId:X8} "
+ $"textFontDid=0x{text?.FontDid ?? 0u:X8} "
+ $"textColor={(text?.FontColor is { } fc ? fc.ToString() : "none")} "
+ $"textSize={text?.Width}x{text?.Height}");
}
// Resolve the popup SKIN the note's tooltip would mount:
// authored P0x48, else fallback to the template's own
// source layout (StartTooltipAtMouse @0x00460E7E).
uint popupLayout = template.TryGetEffectiveProperty(0x48u, out var pl) && pl.UnsignedValue != 0
? (uint)pl.UnsignedValue
: (uint)tl.UnsignedValue;
uint popupRoot = template.TryGetEffectiveProperty(0x47u, out var pr)
? (uint)pr.UnsignedValue : 0u;
if (popupRoot != 0u)
{
ElementInfo? skin = LayoutImporter.ImportInfos(dats, popupLayout, popupRoot);
if (skin is null)
{
Console.WriteLine(
$"[maphouse] popup skin 0x{popupRoot:X8} in 0x{popupLayout:X8} IMPORT NULL");
}
else
{
Console.WriteLine(
$"[maphouse] popup skin 0x{skin.Id:X8} in 0x{popupLayout:X8} type={skin.Type} "
+ $"({skin.Width}x{skin.Height}) textChild(P0x4A)=0x{skin.TooltipTextChildElementId:X8} "
+ $"stateMedia={skin.StateMedia.Count} kids={skin.Children.Count}");
foreach (var m in skin.StateMedia)
Console.WriteLine(
$"[maphouse] skin stateMedia '{m.Key}' file=0x{m.Value.File:X8} drawMode={m.Value.DrawMode}");
DumpSkinTree(skin, " ");
}
}
}
}
}
@ -194,6 +316,20 @@ public sealed class MapHousePanelSlotProbeTests
}
}
private static void DumpSkinTree(ElementInfo info, string indent)
{
foreach (ElementInfo c in info.Children)
{
Console.WriteLine(
$"[maphouse] {indent}skin kid 0x{c.Id:X8} type={c.Type} ({c.X},{c.Y} {c.Width}x{c.Height}) "
+ $"fontDid=0x{c.FontDid:X8} stateMedia={c.StateMedia.Count} kids={c.Children.Count}");
foreach (var m in c.StateMedia)
Console.WriteLine(
$"[maphouse] {indent} media '{m.Key}' file=0x{m.Value.File:X8} drawMode={m.Value.DrawMode}");
DumpSkinTree(c, indent + " ");
}
}
private static bool FindInfo(ElementInfo info, uint id)
{
if (info.Id == id) return true;