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
|
|
@ -25,6 +25,23 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
private readonly Dictionary<uint, (string Text, UiMeterLabelAlign Align)> _stateLabels = new();
|
||||
private (string Text, UiMeterLabelAlign Align)? _activeStateLabel;
|
||||
|
||||
// Vitals ShowDetail icon overlays (see ConfigureDetailOverlay).
|
||||
private bool _detailConfigured;
|
||||
private bool _detailPassToChildren;
|
||||
private uint _detailBackSprite;
|
||||
private uint _detailFrontSprite;
|
||||
private (float X, float Y, float W, float H) _detailBackRect;
|
||||
private (float X, float Y, float W, float H) _detailFrontRect;
|
||||
|
||||
/// <summary>True when this meter absorbed the vitals detail-icon overlays. Exposed for tests.</summary>
|
||||
internal bool HasDetailOverlay => _detailConfigured;
|
||||
/// <summary>The dim back-container detail icon (ShowDetail media). Exposed for tests.</summary>
|
||||
internal uint DetailBackSprite => _detailBackSprite;
|
||||
/// <summary>The bright fill-clipped front-container detail icon. Exposed for tests.</summary>
|
||||
internal uint DetailFrontSprite => _detailFrontSprite;
|
||||
/// <summary>The back overlay's authored meter-local rect. Exposed for tests.</summary>
|
||||
internal (float X, float Y, float W, float H) DetailBackRect => _detailBackRect;
|
||||
|
||||
/// <summary>Dat element id, set by the layout importer so duplicated page copies can be scoped.</summary>
|
||||
public uint ElementId { get; set; }
|
||||
|
||||
|
|
@ -114,8 +131,54 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
_stateLabels[stateId] = (text, align);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the vitals "detail" icon overlays absorbed from the meter's
|
||||
/// two slice containers (the <c>0x100004A9</c> children — health heart
|
||||
/// 0x06007490/91, stamina sword 0x06007492/93, mana scepter 0x06007494/95).
|
||||
/// Retail authors each icon TWICE: a dim version in the BACK container
|
||||
/// (drawn unclipped over the empty track) and a bright version in the
|
||||
/// FRONT container (fill-clipped with the rest of the front layer —
|
||||
/// <c>UIElement_Meter::DrawChildren @0x0046FBD0</c> clips the whole
|
||||
/// <c>m_pcChildImage</c> child, element id 2, to the 0x69 fraction), so
|
||||
/// the icon itself fills up with the vital. Both overlays author media
|
||||
/// ONLY for <c>ShowDetail</c> (<c>HideDetail</c> authors File=0), so they
|
||||
/// draw solely in that state. Rects are the overlays' authored X/Y/W/H
|
||||
/// local to the meter (the containers span the meter at 0,0).
|
||||
/// </summary>
|
||||
internal void ConfigureDetailOverlay(
|
||||
uint backSprite, float backX, float backY, float backW, float backH,
|
||||
uint frontSprite, float frontX, float frontY, float frontW, float frontH,
|
||||
bool passToChildren)
|
||||
{
|
||||
_detailBackSprite = backSprite;
|
||||
_detailBackRect = (backX, backY, backW, backH);
|
||||
_detailFrontSprite = frontSprite;
|
||||
_detailFrontRect = (frontX, frontY, frontW, frontH);
|
||||
_detailConfigured = backSprite != 0 || frontSprite != 0;
|
||||
_detailPassToChildren = passToChildren;
|
||||
}
|
||||
|
||||
public bool TrySetRetailState(uint stateId)
|
||||
{
|
||||
// Vitals detail toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00
|
||||
// flips the window root between HideDetail 0x10000006 and ShowDetail
|
||||
// 0x10000007; the meter receives the state through the authored
|
||||
// PassToChildren cascade — UIElement::SetState @0x00464E70). The
|
||||
// meter's own HideDetail/ShowDetail StateDescs are media-less and
|
||||
// exist purely to keep propagating (PassToChildren=true), so forward
|
||||
// to the absorbed text child (whose per-state 0x3B hides the numbers
|
||||
// in ShowDetail) and let OnDraw key the icon overlays off the state.
|
||||
if (_detailConfigured
|
||||
&& stateId is RetailUiStateIds.HideDetail or RetailUiStateIds.ShowDetail)
|
||||
{
|
||||
ActiveRetailStateId = stateId;
|
||||
if (_detailPassToChildren)
|
||||
foreach (UiElement child in Children)
|
||||
if (child is IUiDatStateful stateful)
|
||||
stateful.TrySetRetailState(stateId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hasFill = _stateFillSprites.TryGetValue(stateId, out uint spriteId);
|
||||
bool hasLabel = _stateLabels.TryGetValue(stateId, out var caption);
|
||||
if (!hasFill && !hasLabel)
|
||||
|
|
@ -198,9 +261,23 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
// drawn at FULL width too but horizontally CLIPPED to the fill fraction.
|
||||
// The front carries its own right-cap (shown at 100%); clipping below 100%
|
||||
// removes it and reveals the back track's right-cap — retail's scissor-fill.
|
||||
//
|
||||
// ShowDetail icon overlays ride their authored containers: the dim BACK
|
||||
// icon draws over the full track (its 0x100004A9 child draws AFTER the
|
||||
// three slice children — higher ReadOrder within the back container);
|
||||
// the bright FRONT icon is clipped with the rest of the front layer to
|
||||
// the fill fraction (retail clips the whole element-id-2 child).
|
||||
bool detail = _detailConfigured
|
||||
&& ActiveRetailStateId == RetailUiStateIds.ShowDetail;
|
||||
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
|
||||
if (detail)
|
||||
DrawDetailIcon(ctx, resolve, _detailBackSprite, _detailBackRect, Width);
|
||||
if (pct is not null && p > 0f)
|
||||
{
|
||||
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
|
||||
if (detail)
|
||||
DrawDetailIcon(ctx, resolve, _detailFrontSprite, _detailFrontRect, Width * p);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -302,6 +379,30 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
ctx.DrawSprite(tex, 0f, y, w, visibleH, 0f, v0, 1f, v1, System.Numerics.Vector4.One);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws one ShowDetail icon overlay at its authored meter-local rect,
|
||||
/// horizontally clipped to <paramref name="clipW"/> local px (the back icon
|
||||
/// passes the full width; the front icon passes <c>Width * fraction</c>,
|
||||
/// mirroring retail's whole-front-container fill clip). The visible portion
|
||||
/// is UV-cropped so the icon reveals left-to-right with the fill. Height is
|
||||
/// clamped to the meter's box — retail clips children to the parent rect
|
||||
/// (the stamina FRONT overlay authors H=28 in a 16px bar; retail shows 16).
|
||||
/// </summary>
|
||||
private void DrawDetailIcon(
|
||||
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint spriteId, (float X, float Y, float W, float H) rect, float clipW)
|
||||
{
|
||||
if (spriteId == 0 || rect.W <= 0f || rect.H <= 0f) return;
|
||||
var (tex, _, _) = resolve(spriteId);
|
||||
if (tex == 0) return;
|
||||
float visibleW = MathF.Min(rect.W, clipW - rect.X);
|
||||
if (visibleW <= 0f) return;
|
||||
float h = MathF.Min(rect.H, Height - rect.Y);
|
||||
if (h <= 0f) return;
|
||||
float u1 = visibleW / rect.W;
|
||||
ctx.DrawSprite(tex, rect.X, rect.Y, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
/// <summary>Draw a slice over local [<paramref name="pieceX"/>,
|
||||
/// pieceX+<paramref name="pieceW"/>], with the texture repeating every
|
||||
/// <paramref name="nativeW"/> px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue