fix(ui): gate — no void frames around the login wormhole; vitals icons centered

The login tunnel now covers from the first world-facing frame (the
sky-void backdrop can never present pre-tunnel) and holds through an
atomic tunnel-to-world swap at reveal completion — the void is
structurally unreachable on both edges, pinned by frame-sequence tests
across WorldSceneRenderer/WorldRevealCoordinator/LocalPlayerTeleport-
Controller/RuntimeWorldTransitState. Vitals detail icons draw at their
authored centered offsets in both stacked and side-by-side layouts.
Implemented and live-probed by the fix agent; finalized by the lead
after the agent parked post-verification (gates re-run green:
App 5512/3, Runtime 1747/0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-17 12:30:51 +02:00
parent 2f8c046aba
commit fdc4fd496d
17 changed files with 649 additions and 66 deletions

View file

@ -542,13 +542,14 @@ public static class DatWidgetFactory
{
bool passToChildren = info.States.Values.Any(
static s => s.Name is "HideDetail" or "ShowDetail" && s.PassToChildren);
// Each spec carries the overlay's authored edge modes and its
// container's authored size: the overlays author L3/R3 (retail
// CENTER anchors), which is what re-centers the icon when the
// vitals window resizes the meter away from its authored
// width (UiMeter.ComputeDetailOverlayRect).
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,
DetailOverlaySpec(backOverlay, containers[0]),
DetailOverlaySpec(frontOverlay, containers[1]),
passToChildren);
}
}
@ -672,6 +673,23 @@ public static class DatWidgetFactory
&& c.StateMedia.TryGetValue("ShowDetail", out var media)
&& media.File != 0);
/// <summary>
/// Builds one absorbed overlay spec: the ShowDetail sprite, the authored
/// container-local rect, the authored raw edge-anchor modes, and the
/// container's authored size (the overlay's reflow parent — the container
/// itself authors L1/T1/R1/B1, so its current box always equals the
/// meter's). A null overlay yields the empty spec (Sprite 0 never draws).
/// </summary>
private static UiMeterDetailOverlaySpec DetailOverlaySpec(
ElementInfo? overlay, ElementInfo container)
=> overlay is null
? default
: new UiMeterDetailOverlaySpec(
overlay.StateMedia["ShowDetail"].File,
overlay.X, overlay.Y, overlay.Width, overlay.Height,
overlay.Left, overlay.Top, overlay.Right, overlay.Bottom,
container.Width, container.Height);
private static bool HasStatefulFill(ElementInfo container)
=> container.States.Any(pair =>
pair.Key != UiStateInfo.DirectStateId

View file

@ -148,6 +148,21 @@ public sealed class RetailUiAutomationProbe
return true;
}
/// <summary>
/// Raw synthetic drag between two canvas points (gate-fix round,
/// 2026-08-17): the vitals icon-centering verify needs the vitals window
/// RESIZED away from its authored width (the retail L3/R3 center anchors
/// only become observable on a non-authored meter width), and window
/// resize rides an edge-grip drag no element/item-addressed drag form can
/// express. Same synthetic <see cref="UiRoot"/> pointer route as
/// <see cref="DragItemToElement"/> — never the OS cursor.
/// </summary>
public bool DragAtPoint(int startX, int startY, int endX, int endY)
{
DragAt(startX, startY, endX, endY);
return true;
}
/// <summary>
/// 2026-08-17 morning gate: synthetic pointer HOVER (no click) at an
/// element's center, for rollover/tooltip verification. Deliberately

View file

@ -286,8 +286,21 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
private bool DoDrag(ScriptCommand command)
{
var p = command.Parts;
if (p.Length >= 6
&& string.Equals(p[1], "at", StringComparison.OrdinalIgnoreCase))
{
// `drag at <x1> <y1> <x2> <y2>` — raw synthetic pointer drag in
// canvas coordinates (gate-fix round 2026-08-17: window-resize
// choreography for the vitals icon-centering verify; see
// RetailUiAutomationProbe.DragAtPoint).
if (!TryParseInt(p[2], out int x1) || !TryParseInt(p[3], out int y1)
|| !TryParseInt(p[4], out int x2) || !TryParseInt(p[5], out int y2))
return Stop(command, "usage: drag at <x1> <y1> <x2> <y2>");
return _probe.DragAtPoint(x1, y1, x2, y2)
|| Stop(command, "drag at failed");
}
if (p.Length < 5 || !string.Equals(p[1], "item", StringComparison.OrdinalIgnoreCase))
return Stop(command, "usage: drag item <guid> element <datId> | drag item <guid> item <guid> | drag item <guid> outside <x> <y>");
return Stop(command, "usage: drag item <guid> element <datId> | drag item <guid> item <guid> | drag item <guid> outside <x> <y> | drag at <x1> <y1> <x2> <y2>");
if (!TryParseUInt(p[2], out uint sourceGuid)) return Stop(command, $"bad item guid '{p[2]}'");
string target = p[3].ToLowerInvariant();

View file

@ -7,6 +7,24 @@ namespace AcDream.App.UI;
/// — the same mapping ElementReader applies at element level).</summary>
public enum UiMeterLabelAlign : byte { Left = 0, Center = 1, Right = 2 }
/// <summary>
/// One absorbed vitals detail-icon overlay (the <c>0x100004A9</c> 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 (<c>ElementDesc</c> Left/Top/Right/Bottom, values 04),
/// and the container's authored size. The overlays author <c>L3/R3</c> —
/// retail's CENTER anchors — so when the meter is resized away from its
/// authored width the icon re-centers through the exact
/// <see cref="UiLayoutPolicy"/> port of
/// <c>UIElement::UpdateForParentSizeChange @0x00462640</c> instead of staying
/// pinned at its authored X (the gate-observed left-drift).
/// </summary>
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);
/// <summary>
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
/// partial-width solid fill, and an optional centered "current/max" numeric
@ -28,19 +46,22 @@ public sealed class UiMeter : UiElement, IUiDatStateful
// 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;
private UiMeterDetailOverlaySpec _detailBack;
private UiMeterDetailOverlaySpec _detailFront;
/// <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;
internal uint DetailBackSprite => _detailBack.Sprite;
/// <summary>The bright fill-clipped front-container detail icon. Exposed for tests.</summary>
internal uint DetailFrontSprite => _detailFrontSprite;
internal uint DetailFrontSprite => _detailFront.Sprite;
/// <summary>The back overlay's authored meter-local rect. Exposed for tests.</summary>
internal (float X, float Y, float W, float H) DetailBackRect => _detailBackRect;
internal (float X, float Y, float W, float H) DetailBackRect =>
(_detailBack.X, _detailBack.Y, _detailBack.W, _detailBack.H);
/// <summary>The complete absorbed back overlay. Exposed for tests.</summary>
internal UiMeterDetailOverlaySpec DetailBack => _detailBack;
/// <summary>The complete absorbed front overlay. Exposed for tests.</summary>
internal UiMeterDetailOverlaySpec DetailFront => _detailFront;
/// <summary>Dat element id, set by the layout importer so duplicated page copies can be scoped.</summary>
public uint ElementId { get; set; }
@ -142,22 +163,69 @@ public sealed class UiMeter : UiElement, IUiDatStateful
/// <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).
/// 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
/// <see cref="ComputeDetailOverlayRect"/> for how those position the icon
/// on a resized meter.
/// </summary>
internal void ConfigureDetailOverlay(
uint backSprite, float backX, float backY, float backW, float backH,
uint frontSprite, float frontX, float frontY, float frontW, float frontH,
in UiMeterDetailOverlaySpec back,
in UiMeterDetailOverlaySpec front,
bool passToChildren)
{
_detailBackSprite = backSprite;
_detailBackRect = (backX, backY, backW, backH);
_detailFrontSprite = frontSprite;
_detailFrontRect = (frontX, frontY, frontW, frontH);
_detailConfigured = backSprite != 0 || frontSprite != 0;
_detailBack = back;
_detailFront = front;
_detailConfigured = back.Sprite != 0 || front.Sprite != 0;
_detailPassToChildren = passToChildren;
}
/// <summary>
/// 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 <c>UIElement::UpdateForParentSizeChange @0x00462640</c> 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 <see cref="UiLayoutPolicy.Apply"/>, the exact port of
/// that retail routine — the overlays' authored <c>L3/R3</c> center
/// anchors are what keep the heart/sword/scepter centered when the
/// vitals window is resized (retail near mode 3 =
/// <c>curParentW/2 - origChildW/2</c> @0x004627ca; far mode 3 =
/// <c>curParentW/2 + origChildW/2 - 1</c> @0x00462827, integer
/// arithmetic, so odd authored widths lose one pixel exactly as retail
/// does).
/// </summary>
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
@ -271,12 +339,22 @@ public sealed class UiMeter : UiElement, IUiDatStateful
&& ActiveRetailStateId == RetailUiStateIds.ShowDetail;
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
if (detail)
DrawDetailIcon(ctx, resolve, _detailBackSprite, _detailBackRect, Width);
{
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, _detailFrontSprite, _detailFrontRect, Width * p);
{
DrawDetailIcon(
ctx, resolve, _detailFront.Sprite,
ComputeDetailOverlayRect(in _detailFront, Width, Height),
Width * p);
}
}
}
}