using System.Numerics; namespace AcDream.App.UI; /// Justification of a meter's absorbed state caption (dat property /// 0x14 on the caption child's own state: 0/2 = Left, 3/5 = Right, 1 = Center /// — the same mapping ElementReader applies at element level). public enum UiMeterLabelAlign : byte { Left = 0, Center = 1, Right = 2 } /// /// One absorbed vitals detail-icon overlay (the 0x100004A9 child of a /// meter's back/front slice container): its ShowDetail sprite, authored rect /// (local to the container, which spans the meter at 0,0), the authored raw /// edge-anchor modes (ElementDesc Left/Top/Right/Bottom, values 0–4), /// and the container's authored size. The overlays author L3/R3 — /// retail's CENTER anchors — so when the meter is resized away from its /// authored width the icon re-centers through the exact /// port of /// UIElement::UpdateForParentSizeChange @0x00462640 instead of staying /// pinned at its authored X (the gate-observed left-drift). /// internal readonly record struct UiMeterDetailOverlaySpec( uint Sprite, float X, float Y, float W, float H, uint LeftMode, uint TopMode, uint RightMode, uint BottomMode, float ParentW, float ParentH); /// /// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a /// partial-width solid fill, and an optional centered "current/max" numeric /// overlay. returns 0..1 (null = no data → empty bar); /// returns the overlay text (null = no number). /// /// /// Solid-color fill + debug font for Spec 1. The retail gradient bar sprite /// (glassy center highlight) and the retail dat font are a later polish pass — /// retail's vitals are bars exactly like this, just sprited. /// /// public sealed class UiMeter : UiElement, IUiDatStateful { private readonly Dictionary _stateFillSprites = new(); private readonly Dictionary _stateLabels = new(); private (string Text, UiMeterLabelAlign Align)? _activeStateLabel; // Vitals ShowDetail icon overlays (see ConfigureDetailOverlay). private bool _detailConfigured; private bool _detailPassToChildren; private UiMeterDetailOverlaySpec _detailBack; private UiMeterDetailOverlaySpec _detailFront; /// True when this meter absorbed the vitals detail-icon overlays. Exposed for tests. internal bool HasDetailOverlay => _detailConfigured; /// The dim back-container detail icon (ShowDetail media). Exposed for tests. internal uint DetailBackSprite => _detailBack.Sprite; /// The bright fill-clipped front-container detail icon. Exposed for tests. internal uint DetailFrontSprite => _detailFront.Sprite; /// The back overlay's authored meter-local rect. Exposed for tests. internal (float X, float Y, float W, float H) DetailBackRect => (_detailBack.X, _detailBack.Y, _detailBack.W, _detailBack.H); /// The complete absorbed back overlay. Exposed for tests. internal UiMeterDetailOverlaySpec DetailBack => _detailBack; /// The complete absorbed front overlay. Exposed for tests. internal UiMeterDetailOverlaySpec DetailFront => _detailFront; /// Dat element id, set by the layout importer so duplicated page copies can be scoped. public uint ElementId { get; set; } /// Fill fraction provider; a null result draws an empty bar. public Func Fill { get; set; } = () => 0f; /// Centered overlay text provider (e.g. "291/291"); null = none. public Func Label { get; set; } = () => null; public Vector4 BarColor { get; set; } = new(1f, 0f, 0f, 1f); public Vector4 BgColor { get; set; } = new(0f, 0f, 0f, 0.5f); public Vector4 LabelColor { get; set; } = new(1f, 1f, 1f, 1f); /// Retail LayoutDesc property 0x21 (two-pass glyph outline, /// UIElement_Text::SetOutline @0x0046a81c). Seeded by DatWidgetFactory /// from the element's effective-default state, same as /// (round-5 review S2 — per-STATE switching is AP-192). public bool Outline { get; set; } /// Retail LayoutDesc property 0x22 (m_curOutlineColor, /// ctor default black). Only meaningful when is true. public Vector4 OutlineColor { get; set; } = UiRenderContext.DefaultOutlineColor; /// Retail dat font (Font 0x40000000) for the "cur/max" overlay. When /// set, the label renders through the dat-font two-pass blit (outline + fill); /// when null, the debug bitmap font /// is used instead. Set by the host when the retail UI is active. public UiDatFont? DatFont { get; set; } /// Resolver from a RenderSurface DataId to (GL handle, w, h). When set /// with the 9-slice ids below, the bar draws the retail sprites instead of solid color. public Func? SpriteResolve { get; set; } // Retail vital bars are a horizontal 3-slice: a fixed-width bevelled left-cap, // a TILED gradient middle (the "fill-tile" repeats at native width — it does not // stretch), and a fixed-width right-cap. The "back" slice is the empty track // (drawn full width); the "front" slice is the coloured fill (drawn full-geometry // but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's // shows through when partial). Ids come from the stacked vitals LayoutDesc // (0x2100006C) via the dump-vitals-layout CLI; 0 = none. /// Empty-track left-cap RenderSurface id. public uint BackLeft { get; set; } /// Empty-track middle (tiled gradient) RenderSurface id. public uint BackTile { get; set; } /// Empty-track right-cap RenderSurface id. public uint BackRight { get; set; } /// Coloured-fill left-cap RenderSurface id. public uint FrontLeft { get; set; } /// Coloured-fill middle (tiled gradient) RenderSurface id. public uint FrontTile { get; set; } /// Coloured-fill right-cap RenderSurface id. public uint FrontRight { get; set; } /// The active numeric DAT state for a stateful fill meter. public uint ActiveRetailStateId { get; private set; } /// The caption latched by the active retail state (null when the /// state authors none). Exposed for tests. internal string? ActiveStateLabel => _activeStateLabel?.Text; /// The latched caption's authored justification. Exposed for tests. internal UiMeterLabelAlign? ActiveStateLabelAlign => _activeStateLabel?.Align; /// /// Registers a fill sprite carried by a child state of the imported meter. /// Used by the powerbar shape in LayoutDesc 0x21000072, whose track is on /// the meter and whose Jump/Melee/Missile/DDD fills are states of one child. /// internal void ConfigureStateFill(uint stateId, uint spriteId) { if (stateId != 0 && spriteId != 0) _stateFillSprites[stateId] = spriteId; } /// /// Registers a caption carried by a state of the meter's absorbed Type-12 /// child (gmPowerbarUI's 0x10000035: JumpMode 'Height' / MeleeMode 'Power' /// / MissileMode 'Accuracy'). Retail shows it via the meter's /// PassToChildren state cascade; the absorbed equivalent latches it in /// and draws it when no live /// provider supplies text. Each mode STATE authors its /// own justification (dat property 0x14 — the powerbar modes author 0x3 = /// Right; only the element default is centered), so alignment rides the /// state entry. /// internal void ConfigureStateLabel(uint stateId, string text, UiMeterLabelAlign align) { if (stateId != 0 && !string.IsNullOrEmpty(text)) _stateLabels[stateId] = (text, align); } /// /// Registers the vitals "detail" icon overlays absorbed from the meter's /// two slice containers (the 0x100004A9 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 — /// UIElement_Meter::DrawChildren @0x0046FBD0 clips the whole /// m_pcChildImage child, element id 2, to the 0x69 fraction), so /// the icon itself fills up with the vital. Both overlays author media /// ONLY for ShowDetail (HideDetail authors File=0), so they /// draw solely in that state. Each spec carries the overlay's authored /// rect (local to its container, which spans the meter at 0,0), its raw /// edge-anchor modes, and the container's authored size — see /// for how those position the icon /// on a resized meter. /// internal void ConfigureDetailOverlay( in UiMeterDetailOverlaySpec back, in UiMeterDetailOverlaySpec front, bool passToChildren) { _detailBack = back; _detailFront = front; _detailConfigured = back.Sprite != 0 || front.Sprite != 0; _detailPassToChildren = passToChildren; } /// /// The overlay's effective meter-local rect at the meter's CURRENT size. /// At the authored container size the authored rect stands verbatim — /// retail's UIElement::UpdateForParentSizeChange @0x00462640 runs /// only when a parent actually resizes, and the designers hand-placed /// rects that are not always the exact center formula (the stamina sword /// authors X=32 where the mode-3 formula yields 33). On any other size /// the authored edge modes are applied from the ORIGINAL child/parent /// rects through , the exact port of /// that retail routine — the overlays' authored L3/R3 center /// anchors are what keep the heart/sword/scepter centered when the /// vitals window is resized (retail near mode 3 = /// curParentW/2 - origChildW/2 @0x004627ca; far mode 3 = /// curParentW/2 + origChildW/2 - 1 @0x00462827, integer /// arithmetic, so odd authored widths lose one pixel exactly as retail /// does). /// internal static (float X, float Y, float W, float H) ComputeDetailOverlayRect( in UiMeterDetailOverlaySpec overlay, float meterW, float meterH) { int parentW = (int)meterW, parentH = (int)meterH; int origParentW = (int)overlay.ParentW, origParentH = (int)overlay.ParentH; if (origParentW <= 0 || origParentH <= 0 || (parentW == origParentW && parentH == origParentH)) { return (overlay.X, overlay.Y, overlay.W, overlay.H); } var originalChild = UiPixelRect.FromPositionAndSize( (int)overlay.X, (int)overlay.Y, (int)overlay.W, (int)overlay.H); var originalParent = UiPixelRect.FromPositionAndSize( 0, 0, origParentW, origParentH); var currentParent = UiPixelRect.FromPositionAndSize( 0, 0, parentW, parentH); UiPixelRect effective = UiLayoutPolicy.Apply( overlay.LeftMode, overlay.TopMode, overlay.RightMode, overlay.BottomMode, originalChild, originalParent, originalChild, currentParent); return (effective.X0, effective.Y0, effective.Width, effective.Height); } 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) return false; ActiveRetailStateId = stateId; if (hasFill) { FrontLeft = 0; FrontTile = spriteId; FrontRight = 0; } _activeStateLabel = hasLabel ? caption : null; return true; } /// Vertical orientation (retail m_eDirection 2/4). Default false = /// horizontal (direction 1). The burden bar is the only vertical meter today. public bool Vertical { get; set; } /// For a vertical meter, fill grows from the bottom up (retail direction 4) /// when true, top-down (direction 2) when false. Default bottom-up. Visual-confirmed. public bool FillFromBottom { get; set; } = true; public UiMeter() { ClickThrough = true; } /// The meter draws its own 3-slice bars; the importer must not build its /// grandchild slice/text elements as separate widgets. public override bool ConsumesDatChildren => true; private static float AlignX(UiMeterLabelAlign align, float width, float textWidth) => align switch { UiMeterLabelAlign.Left => 0f, UiMeterLabelAlign.Right => width - textWidth, _ => (width - textWidth) * 0.5f, }; /// Clamp to [0,1] and return the fill rect /// (local px) for a bar of x . public static (float x, float y, float w, float h) ComputeFillRect( float pct, float w, float h) { if (pct < 0f) pct = 0f; if (pct > 1f) pct = 1f; return (0f, 0f, w * pct, h); } /// Clamp to [0,1] and return the vertical fill rect /// (local px). true → the fill occupies the bottom /// h*pct px (retail direction 4); false → the top (direction 2). public static (float x, float y, float w, float h) ComputeVFillRect( float pct, float w, float h, bool fromBottom) { if (pct < 0f) pct = 0f; if (pct > 1f) pct = 1f; float fh = h * pct; return (0f, fromBottom ? h - fh : 0f, w, fh); } protected override void OnDraw(UiRenderContext ctx) { float? pct = Fill(); float p = pct is float pf ? (pf < 0f ? 0f : pf > 1f ? 1f : pf) : 0f; if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0)) { if (Vertical) { // Track full height, fill clipped to the fraction along the fill direction. // The burden meter is a single-tile bar (BackTile/FrontTile, no caps). DrawVBar(ctx, resolve, BackTile, Height, fromBottom: FillFromBottom, isFill: false); if (pct is not null && p > 0f) DrawVBar(ctx, resolve, FrontTile, Height * p, fromBottom: FillFromBottom, isFill: true); } else { // Retail meter (UIElement_Meter::DrawChildren): the BACK 3-slice is the // empty track, drawn full width; the FRONT 3-slice is the coloured fill, // 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, _detailBack.Sprite, ComputeDetailOverlayRect(in _detailBack, Width, Height), Width); } if (pct is not null && p > 0f) { DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p); if (detail) { DrawDetailIcon( ctx, resolve, _detailFront.Sprite, ComputeDetailOverlayRect(in _detailFront, Width, Height), Width * p); } } } } else { // Placeholder solid-color fallback. ctx.DrawRect(0, 0, Width, Height, BgColor); if (pct is not null && p > 0f) { var (fx, fy, fw, fh) = ComputeFillRect(p, Width, Height); if (fw > 0f) ctx.DrawRect(fx, fy, fw, fh, BarColor); } } // A live provider (vitals "cur/max") wins and stays centered; // otherwise the active retail-state caption shows with its own // authored justification (the powerbar modes author Right). string? label = Label(); UiMeterLabelAlign align = UiMeterLabelAlign.Center; if (string.IsNullOrEmpty(label) && _activeStateLabel is { } stateLabel) (label, align) = stateLabel; if (!string.IsNullOrEmpty(label)) { if (DatFont is { } datFont) { // Retail path: the dat font's two-pass blit. float tw = datFont.MeasureWidth(label); float tx = AlignX(align, Width, tw); float ty = (Height - datFont.LineHeight) * 0.5f; ctx.DrawStringDat(datFont, label, tx, ty, LabelColor, Outline, OutlineColor); } else if (ctx.DefaultFont is { } font) { // Fallback: debug bitmap font (no dat font available). float tw = font.MeasureWidth(label); float tx = AlignX(align, Width, tw); float ty = (Height - font.LineHeight) * 0.5f; ctx.DrawString(label, tx, ty, LabelColor); } } } /// /// Draws the full-width horizontal 3-slice (native-width left-cap, stretched /// middle, native-width right-cap) over this meter's rect, horizontally CLIPPED /// so nothing past (local px from the left) is drawn. /// The back track passes clipW = Width; the front fill passes /// clipW = Width * fraction. Clipping UV-crops each slice proportionally, /// so the fill ends cleanly and the back's right-cap shows through when partial. /// A 0 id skips that slice. /// private void DrawHBar( UiRenderContext ctx, Func resolve, uint leftId, uint midId, uint rightId, float clipW) { if (clipW <= 0f) return; float w = Width, h = Height; // Only resolve a slice when its id is non-zero. resolve(0) returns the 1x1 MAGENTA // placeholder with a NON-ZERO GL handle, so resolving a zero (absent) cap id and then // testing `tex != 0` would draw a 1px magenta cap. The single-image meter (toolbar // selected-object bar) has no left/right caps (ids 0); the 3-slice vitals meter sets // all six ids. Guard on the id, not the resolved handle. var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (0u, 0, 0); var (mt, mw, _) = midId != 0 ? resolve(midId) : (0u, 0, 0); var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (0u, 0, 0); float capL = lt != 0 ? MathF.Min(lw, w) : 0f; float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f; float midW = w - capL - capR; // Each slice's texture repeats every NATIVE-width px (UV-repeat; the UI // texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their // own native width → a single 1:1 copy. The wide middle spans many native // widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather // than stretching one copy. (Same UV-repeat the chrome border already uses.) DrawPiece(ctx, lt, 0f, capL, lw, h, clipW); DrawPiece(ctx, mt, capL, midW, mw, h, clipW); DrawPiece(ctx, rt, w - capR, capR, rw, h, clipW); } /// Draws a single-tile vertical bar slice. The track ( /// false) spans the full height; the fill spans px from the /// bottom () or top, UV-cropped to that fraction of the /// sprite so the fill reveals the matching part of the art (retail /// UIElement_Meter::DrawChildren Box2D clip, direction 2/4). A 0 id is a no-op. private void DrawVBar(UiRenderContext ctx, Func resolve, uint tileId, float visibleH, bool fromBottom, bool isFill) { if (tileId == 0 || visibleH <= 0f) return; var (tex, _, _) = resolve(tileId); if (tex == 0) return; float w = Width, h = Height; if (visibleH > h) visibleH = h; float frac = h > 0f ? visibleH / h : 0f; // Bottom-up fill: bottom of the rect AND bottom of the sprite (v in [1-frac, 1]). // Top-down / track: top of the rect AND top of the sprite (v in [0, frac]). float y = isFill && fromBottom ? h - visibleH : 0f; float v0 = isFill && fromBottom ? 1f - frac : 0f; float v1 = isFill && fromBottom ? 1f : frac; ctx.DrawSprite(tex, 0f, y, w, visibleH, 0f, v0, 1f, v1, System.Numerics.Vector4.One); } /// /// Draws one ShowDetail icon overlay at its authored meter-local rect, /// horizontally clipped to local px (the back icon /// passes the full width; the front icon passes Width * fraction, /// 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). /// private void DrawDetailIcon( UiRenderContext ctx, Func 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); } /// Draw a slice over local [, /// pieceX+], with the texture repeating every /// px (UV-repeat — the UI texture is GL_REPEAT-wrapped). /// Clipped so nothing past shows. For a cap (span == native) /// this is one 1:1 copy; for the wide middle it tiles; a partial last copy is /// UV-cropped. private static void DrawPiece( UiRenderContext ctx, uint tex, float pieceX, float pieceW, float nativeW, float h, float clipW) { if (tex == 0 || pieceW <= 0f || nativeW <= 0f) return; float visibleW = MathF.Min(pieceW, clipW - pieceX); if (visibleW <= 0f) return; float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One); } }