feat(ui): vitals — click toggles retail's numeric/graphical detail modes
Port of gmVitalsUI's press toggle, derived end-to-end from the named
retail decomp + the authored DAT data (installed-DAT probe 2026-08-17):
- gmVitalsUI::ListenToElementMessage @0x004BFC00: mouse press (msg 0x1C,
dwParam1 7=left or 0xA=right — the same param pair the spellbook's
select/favorite handler @0x0048C033 disambiguates) flips
SetState(m_state == HideDetail ? ShowDetail : HideDetail). Both floaty
subclasses (gmFloatyVitalsUI 0x1000004D / gmFloatySideVitalsUI
0x10000056) inherit it verbatim.
- UIElement::SetState @0x00464E70 cascades through the authored
PassToChildren chain: root and meters author media-less
HideDetail/ShowDetail StateDescs with PassToChildren=true.
- HideDetail (0x10000006) = the NUMERIC mode: the cur/max labels author
{0x3B:false} (0x3B = invisible; UIElement::OnSetAttribute case 8
@0x00462DAE is SetVisible(value == 0)), the 0x100004A9 overlays author
File=0.
- ShowDetail (0x10000007) = the GRAPHICAL mode: labels author {0x3B:true}
(numbers hidden); each bar shows its authored icon pair — dim back icon
unclipped over the track, bright front icon clipped with the front
container to the fill fraction (UIElement_Meter::DrawChildren
@0x0046FBD0 clips the whole element-id-2 child; m_pcChildImage =
GetChildRecursive(this, 2) @0x0046F7E3). Health heart 0x06007490/91
(18x16 @66,0), stamina sword 0x06007492/93 (85x16 @32,0), mana scepter
0x06007494/95 (100x16 @25,0) — identical authoring in both 0x2100006C
and 0x21000075.
- Initial state is the authored Undef (numbers visible, no icons —
visually HideDetail); retail's first press lands on HideDetail, then
the pair toggles forever. NOT persisted: SaveScreenLayout @0x004EAD50
writes window rects only, and no PlayerModule option is touched — the
mode resets per session, per window.
- Presses on drag bars / resize grips do not toggle: retail's
UIElement_Dragbar @0x0046C850 and UIElement_Resizebar @0x0046B930
consume the press (return 2) before it can bubble to the root.
Implementation: new UiVitalsRoot behavioral widget registered for the
three gmVitals class ids (press handler + state flip over the existing
UiDatElement state machine); UiMeter absorbs the two 0x100004A9 overlays
(ConfigureDetailOverlay + ShowDetail-keyed draw, back unclipped / front
fill-clipped) and forwards the detail states to its absorbed text child;
UiText.ApplyDatState gains the same named-state-only 0x3B honor
UiDatElement already had (the DirectState 0x3B class stays gated — #408).
8 new fixture-driven conformance tests (toggle sequence, right-press,
label cascade, chrome exclusions, per-window independence, overlay
extraction). App suite Release live-DAT: 5495 passed / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
302d90209d
commit
306a1670d3
6 changed files with 480 additions and 10 deletions
|
|
@ -27,10 +27,12 @@ namespace AcDream.App.UI.Layout;
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The expand-detail overlay present in the front container carries ONLY named
|
||||
/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
|
||||
/// <c>TryGetValue("")</c> filter in <see cref="SliceIds"/> excludes it
|
||||
/// automatically.
|
||||
/// The expand-detail icon overlay present in EACH container (back + front)
|
||||
/// carries ONLY named states ("HideDetail"/"ShowDetail") — no "" DirectState
|
||||
/// entry — so the <c>TryGetValue("")</c> filter in <see cref="SliceIds"/>
|
||||
/// excludes it from slice extraction; <see cref="DetailOverlay"/> then absorbs
|
||||
/// it into <see cref="UiMeter.ConfigureDetailOverlay"/> for the retail
|
||||
/// HideDetail/ShowDetail click toggle.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class DatWidgetFactory
|
||||
|
|
@ -76,6 +78,15 @@ public static class DatWidgetFactory
|
|||
UiElement e = info.Type switch
|
||||
{
|
||||
UiRadar.RetailClassId => new UiRadar(), // gmRadarUI (Register 0x004D8B80)
|
||||
// The vitals window roots — gmVitalsUI (0x10000009 @0x004BFE10),
|
||||
// gmFloatyVitalsUI (0x1000004D, stacked 0x2100006C), and
|
||||
// gmFloatySideVitalsUI (0x10000056, side-by-side 0x21000075). All
|
||||
// three share the inherited HideDetail/ShowDetail press toggle
|
||||
// (gmVitalsUI::ListenToElementMessage @0x004BFC00) — see UiVitalsRoot.
|
||||
UiVitalsRoot.GmVitalsClassId
|
||||
or UiVitalsRoot.GmFloatyVitalsClassId
|
||||
or UiVitalsRoot.GmFloatySideVitalsClassId
|
||||
=> new UiVitalsRoot(info, resolve),
|
||||
1 => BuildButton(info, resolve, elementFont, fontResolve, stringResolve), // UIElement_Button
|
||||
2 => new UiDatElement(info, resolve) // UIElement_Dragbar (Register @ 0x0046C840)
|
||||
{
|
||||
|
|
@ -446,8 +457,11 @@ public static class DatWidgetFactory
|
|||
/// │ ├── left-cap image (→ front-left sprite)
|
||||
/// │ ├── center image (→ front-tile sprite)
|
||||
/// │ ├── right-cap image (→ front-right sprite)
|
||||
/// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
|
||||
/// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController)
|
||||
/// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState —
|
||||
/// │ absorbed as the bright fill-clipped detail icon; the back
|
||||
/// │ container has a matching dim one — see ConfigureDetailOverlay)
|
||||
/// └── text label (Type 0) (built as a real UiText child by LayoutImporter's meter
|
||||
/// carve-out; Fill/Label providers bound by VitalsController)
|
||||
/// </code>
|
||||
/// </para>
|
||||
///
|
||||
|
|
@ -508,6 +522,35 @@ public static class DatWidgetFactory
|
|||
m.FrontLeft = fl;
|
||||
m.FrontTile = ft;
|
||||
m.FrontRight = fr;
|
||||
|
||||
// The expand-detail icon overlays (the 0x100004A9 child of EACH
|
||||
// container — dim icon in the back track, bright icon in the
|
||||
// fill-clipped front layer; health heart 0x06007490/91, stamina
|
||||
// sword 0x06007492/93, mana scepter 0x06007494/95). They author
|
||||
// media only for the named ShowDetail state (HideDetail authors
|
||||
// File=0), which is why SliceIds' DirectState filter already
|
||||
// excludes them from the slice extraction above. Absorb them into
|
||||
// the meter alongside the slices so the HideDetail/ShowDetail
|
||||
// click toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00)
|
||||
// can draw retail's graphical vitals mode. PassToChildren on the
|
||||
// meter's own media-less HideDetail/ShowDetail StateDescs gates
|
||||
// the cascade to the absorbed number label
|
||||
// (UIElement::SetState @0x00464E70).
|
||||
ElementInfo? backOverlay = DetailOverlay(containers[0]);
|
||||
ElementInfo? frontOverlay = DetailOverlay(containers[1]);
|
||||
if (backOverlay is not null || frontOverlay is not null)
|
||||
{
|
||||
bool passToChildren = info.States.Values.Any(
|
||||
static s => s.Name is "HideDetail" or "ShowDetail" && s.PassToChildren);
|
||||
m.ConfigureDetailOverlay(
|
||||
backOverlay is not null ? backOverlay.StateMedia["ShowDetail"].File : 0u,
|
||||
backOverlay?.X ?? 0f, backOverlay?.Y ?? 0f,
|
||||
backOverlay?.Width ?? 0f, backOverlay?.Height ?? 0f,
|
||||
frontOverlay is not null ? frontOverlay.StateMedia["ShowDetail"].File : 0u,
|
||||
frontOverlay?.X ?? 0f, frontOverlay?.Y ?? 0f,
|
||||
frontOverlay?.Width ?? 0f, frontOverlay?.Height ?? 0f,
|
||||
passToChildren);
|
||||
}
|
||||
}
|
||||
else if (containers.Count == 1 && containers[0].StateMedia.ContainsKey(""))
|
||||
{
|
||||
|
|
@ -617,6 +660,18 @@ public static class DatWidgetFactory
|
|||
=> container.Children.Count(c =>
|
||||
c.StateMedia.TryGetValue("", out var media) && media.File != 0) >= 3;
|
||||
|
||||
/// <summary>
|
||||
/// Finds a container's expand-detail icon overlay: the child that authors a
|
||||
/// non-zero image for the named ShowDetail state and NO DirectState image
|
||||
/// (the 0x100004A9 shape in both vitals layouts 0x2100006C / 0x21000075).
|
||||
/// Returns null when the container has none (non-vitals meters).
|
||||
/// </summary>
|
||||
private static ElementInfo? DetailOverlay(ElementInfo container)
|
||||
=> container.Children.FirstOrDefault(static c =>
|
||||
!c.StateMedia.ContainsKey("")
|
||||
&& c.StateMedia.TryGetValue("ShowDetail", out var media)
|
||||
&& media.File != 0);
|
||||
|
||||
private static bool HasStatefulFill(ElementInfo container)
|
||||
=> container.States.Any(pair =>
|
||||
pair.Key != UiStateInfo.DirectStateId
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue