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:
parent
2f8c046aba
commit
fdc4fd496d
17 changed files with 649 additions and 66 deletions
|
|
@ -239,11 +239,13 @@ internal sealed class FrameRootCompositionPhase
|
|||
// deleted at slice V11. The graph is the clear pass, the private-
|
||||
// presentation phase, and the retained UI inside it — the client's
|
||||
// own frame, drawn entirely through the RHI.
|
||||
var teleportRenderState =
|
||||
new LocalPlayerTeleportRenderStateSource(session.LocalTeleport);
|
||||
var renderLoginState = new RenderLoginStateSource(
|
||||
d.Options.LiveMode,
|
||||
d.PlayerMode);
|
||||
var teleportRenderState =
|
||||
new LocalPlayerTeleportRenderStateSource(
|
||||
session.LocalTeleport,
|
||||
renderLoginState);
|
||||
// Campaign V slice V6i-3: on Vulkan the frame's clear is a load op of the
|
||||
// world pass rather than a pass of its own, so the two phases share this
|
||||
// one value. See VulkanWorldScenePhase for why the merge is required
|
||||
|
|
@ -561,6 +563,24 @@ internal sealed class FrameRootCompositionPhase
|
|||
lifecycleAutomation)
|
||||
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
||||
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
||||
if (RenderPresentationDiagnostics.ProbeLoginFrames)
|
||||
{
|
||||
// Enter-world gate round (2026-08-17): the per-frame presentation
|
||||
// classification probe (world/tunnel/black/void transitions). The
|
||||
// tunnel fact is the controller's RAW portal-scene visibility, not
|
||||
// the composed render-state source, so the probe can distinguish
|
||||
// "covered black" from "tunnel scene drawn".
|
||||
var loginFrameProbe = new LoginPresentationFrameProbe(
|
||||
() => session.LocalTeleport.IsPortalViewportVisible,
|
||||
renderLoginState,
|
||||
d.Log);
|
||||
postDiagnostics =
|
||||
postDiagnostics is NullRenderFramePostDiagnosticsPhase
|
||||
? loginFrameProbe
|
||||
: new SerialRenderFramePostDiagnosticsPhase(
|
||||
postDiagnostics,
|
||||
loginFrameProbe);
|
||||
}
|
||||
var renderFrame = new RenderFrameOrchestrator(
|
||||
host.GpuFrameLifetime,
|
||||
// Campaign V slice V8: the Vulkan arm measures the frame bracket
|
||||
|
|
|
|||
95
src/AcDream.App/Rendering/LoginPresentationFrameProbe.cs
Normal file
95
src/AcDream.App/Rendering/LoginPresentationFrameProbe.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic owner for the render-presentation probe family (CLAUDE.md Code
|
||||
/// Structure Rules §5 — one static class per subsystem, typed properties read
|
||||
/// from the environment once at startup, never per-call-site
|
||||
/// <c>GetEnvironmentVariable</c> reads).
|
||||
/// </summary>
|
||||
internal static class RenderPresentationDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Enter-world gate round (2026-08-17): per-frame presentation
|
||||
/// classification for the login wormhole edges. When set, every completed
|
||||
/// render frame is classified by WHAT PRESENTED — <c>world</c> /
|
||||
/// <c>tunnel</c> / <c>black</c> / <c>void</c> — and a <c>[login-frames]</c>
|
||||
/// line is written on every classification transition. The gate contract
|
||||
/// is retail's: the sequence over a login must contain NO <c>void</c>
|
||||
/// entry on either edge (black → tunnel → world, each swap atomic).
|
||||
/// Not a user setting; not in <c>RuntimeOptions</c>; not persisted.
|
||||
/// </summary>
|
||||
public static bool ProbeLoginFrames { get; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_LOGIN_FRAMES") == "1";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The frame-level truth for the login wormhole gate: classifies each
|
||||
/// completed render frame from the same outcome facts the orchestrator
|
||||
/// publishes, plus the two raw presentation inputs (actual tunnel-scene
|
||||
/// visibility and the live waiting-for-login latch), and logs one line per
|
||||
/// TRANSITION so the exact frame sequence at both wormhole edges is
|
||||
/// auditable.
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>world</c> — the normal world viewport drew
|
||||
/// (<see cref="WorldRenderFrameOutcome.NormalWorldDrawn"/>).</description></item>
|
||||
/// <item><description><c>tunnel</c> — the portal-space scene was visible and
|
||||
/// drew over the frame's opaque black.</description></item>
|
||||
/// <item><description><c>black</c> — the frame presented the portal-viewport
|
||||
/// black clear with NO tunnel scene (retail's empty pre-player SmartBox:
|
||||
/// the gameplay screen before CreatePlayer draws no world and no
|
||||
/// tunnel).</description></item>
|
||||
/// <item><description><c>void</c> — the world path ran but drew nothing
|
||||
/// (the sky-only "waiting for login" backdrop, or an unavailable world
|
||||
/// generation). Retail NEVER presents this during a login — any void entry
|
||||
/// in a login sequence is the gate defect.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal sealed class LoginPresentationFrameProbe : IRenderFramePostDiagnosticsPhase
|
||||
{
|
||||
private readonly Func<bool> _tunnelSceneVisible;
|
||||
private readonly IRenderLoginStateSource _login;
|
||||
private readonly Action<string> _log;
|
||||
private long _frame;
|
||||
private double _elapsedSeconds;
|
||||
private string? _lastClass;
|
||||
|
||||
public LoginPresentationFrameProbe(
|
||||
Func<bool> tunnelSceneVisible,
|
||||
IRenderLoginStateSource login,
|
||||
Action<string> log)
|
||||
{
|
||||
_tunnelSceneVisible = tunnelSceneVisible
|
||||
?? throw new ArgumentNullException(nameof(tunnelSceneVisible));
|
||||
_login = login ?? throw new ArgumentNullException(nameof(login));
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
}
|
||||
|
||||
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
{
|
||||
_frame++;
|
||||
_elapsedSeconds += input.DeltaSeconds;
|
||||
|
||||
bool world = outcome.World.NormalWorldDrawn;
|
||||
bool tunnel = _tunnelSceneVisible();
|
||||
bool cover = outcome.Presentation.PortalViewportDrawn;
|
||||
bool waiting = _login.IsWaitingForLogin;
|
||||
|
||||
string presentClass =
|
||||
world ? "world"
|
||||
: tunnel ? "tunnel"
|
||||
: cover ? "black"
|
||||
: "void";
|
||||
|
||||
if (presentClass == _lastClass)
|
||||
return;
|
||||
_lastClass = presentClass;
|
||||
_log(
|
||||
$"[login-frames] frame={_frame} t={_elapsedSeconds:F3}s "
|
||||
+ $"present={presentClass} waiting={(waiting ? 1 : 0)} "
|
||||
+ $"cover={(cover ? 1 : 0)} tunnel={(tunnel ? 1 : 0)} "
|
||||
+ $"world={(world ? 1 : 0)}");
|
||||
}
|
||||
}
|
||||
|
|
@ -136,13 +136,35 @@ internal sealed class LocalPlayerTeleportRenderStateSource
|
|||
: IRenderFramePortalStateSource
|
||||
{
|
||||
private readonly LocalPlayerTeleportController _teleport;
|
||||
private readonly IRenderLoginStateSource _login;
|
||||
|
||||
public LocalPlayerTeleportRenderStateSource(LocalPlayerTeleportController teleport)
|
||||
public LocalPlayerTeleportRenderStateSource(
|
||||
LocalPlayerTeleportController teleport,
|
||||
IRenderLoginStateSource login)
|
||||
{
|
||||
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
|
||||
_login = login ?? throw new ArgumentNullException(nameof(login));
|
||||
}
|
||||
|
||||
public bool IsPortalViewportVisible => _teleport.IsPortalViewportVisible;
|
||||
/// <summary>
|
||||
/// The frame presents the portal-viewport shape (opaque black clear, no
|
||||
/// world draw, retained UI on top) when the tunnel scene is visible OR
|
||||
/// while a live login is still pre-world. The second arm is retail's
|
||||
/// pre-player gameplay screen: after char-select Enter queues UI mode
|
||||
/// 0x10000008 (<c>CM_Login::SendNotice_BeginEnterWorld @ 0x006AD810</c>
|
||||
/// from <c>CPlayerSystem::LogOnCharacter @ 0x0055F890</c>), the SmartBox
|
||||
/// has no player and draws NO world — the screen behind the UI is black
|
||||
/// until <c>SmartBox::teleport_in_progress @ 0x00451C20</c> goes high at
|
||||
/// CreatePlayer and <c>gmSmartBoxUI::UseTime @ 0x004D6EAB</c> begins the
|
||||
/// tunnel in the same tick. The former sky-only "waiting for login"
|
||||
/// backdrop had no retail counterpart and presented as the gate's
|
||||
/// entry-edge VOID (2026-08-17). Both flags flip on the update thread
|
||||
/// (the login activation tick flips ChaseModeEverEntered AND makes the
|
||||
/// tunnel visible before the next render), so the black → tunnel → world
|
||||
/// sequence swaps atomically per frame.
|
||||
/// </summary>
|
||||
public bool IsPortalViewportVisible =>
|
||||
_teleport.IsPortalViewportVisible || _login.IsWaitingForLogin;
|
||||
|
||||
public uint ActiveDestinationCell => _teleport.ActiveDestinationCell;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,11 +159,13 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
|||
_sky.DayFraction);
|
||||
}
|
||||
|
||||
// Retail keeps the live sky during EnterWorld while suppressing
|
||||
// terrain and object geometry until chase mode has engaged.
|
||||
if (_login.IsWaitingForLogin)
|
||||
return CompleteSkippedWorld();
|
||||
|
||||
// The former sky-only "waiting for login" skip lived here.
|
||||
// It is unreachable now: LocalPlayerTeleportRenderStateSource
|
||||
// folds IsWaitingForLogin into PortalViewportVisible (retail's
|
||||
// pre-player gameplay screen draws NO world — black, not sky),
|
||||
// so the portal-visible return above already covers every
|
||||
// waiting frame. One gate computes the frame's visibility;
|
||||
// this phase only enforces it.
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
}
|
||||
|
|
@ -291,16 +293,6 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
|||
diagnostic.VisibleLandblocks,
|
||||
diagnostic.TotalLandblocks,
|
||||
NormalWorldDrawn: true);
|
||||
|
||||
WorldRenderFrameOutcome CompleteSkippedWorld()
|
||||
{
|
||||
CompleteWorldFrame();
|
||||
worldFrameStarted = false;
|
||||
pviewFrameStarted = false;
|
||||
_selection?.CompleteFrame();
|
||||
selectionFrameStarted = false;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
catch (Exception renderFailure)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1161,11 +1161,23 @@ internal sealed class LocalPlayerTeleportController
|
|||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.Place:
|
||||
// No login Place edge: the canonical initial placement is
|
||||
// No login PLACEMENT: the canonical initial placement is
|
||||
// the first-entry conductor's, already committed (the
|
||||
// worldReady latch above requires it). Retail's login
|
||||
// analogue only flips position_update_complete
|
||||
// (SmartBox::UseTime @ 0x00455483).
|
||||
// (SmartBox::UseTime @ 0x00455483) — but that same
|
||||
// blocking-ends edge is where retail RESUMES
|
||||
// CObjectMaint/CPhysics (@ 0x00455410), while the tunnel
|
||||
// is still in front. Acknowledge the login
|
||||
// materialization/simulation release here, the exact
|
||||
// login mirror of the teleport pump's
|
||||
// ObserveMaterialized — without it the world generation
|
||||
// stayed unavailable until Complete, so the whole
|
||||
// WorldFadeIn second presented an EMPTY world frame (the
|
||||
// 2026-08-17 gate's exit-edge void).
|
||||
_worldReveal.ObserveLoginMaterialized(revealGeneration);
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.PlayExitSound:
|
||||
// Release destination cell blocking at the exact
|
||||
|
|
|
|||
|
|
@ -244,6 +244,22 @@ internal sealed class WorldRevealCoordinator
|
|||
return acknowledged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The login mirror of <see cref="ObserveMaterialized"/>: acknowledges
|
||||
/// the login reveal's materialization/simulation-release edge (retail
|
||||
/// resumes <c>CObjectMaint</c>/<c>CPhysics</c> when destination cells
|
||||
/// stop blocking, <c>SmartBox::UseTime @ 0x00455483</c> — while the
|
||||
/// tunnel is still in front). See
|
||||
/// <c>RuntimeWorldTransitState.AcknowledgeLoginMaterialized</c>.
|
||||
/// </summary>
|
||||
public bool ObserveLoginMaterialized(long generation)
|
||||
{
|
||||
bool acknowledged =
|
||||
_transit.AcknowledgeLoginMaterialized(generation);
|
||||
RetryPendingHostWork();
|
||||
return acknowledged;
|
||||
}
|
||||
|
||||
public void ObserveWorldViewportVisible()
|
||||
{
|
||||
RetryPendingHostWork();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 0–4),
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,6 +649,64 @@ public sealed class RuntimeWorldTransitState
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The LOGIN half of the materialization/simulation edge. Retail's
|
||||
/// <c>SmartBox::UseTime @ 0x00455410</c> <c>blocking_for_cells</c> branch
|
||||
/// resumes <c>CObjectMaint</c>/<c>CPhysics</c> the moment destination
|
||||
/// cells stop blocking — while the tunnel is still in front, and
|
||||
/// IDENTICALLY for the initial login and an F751 teleport (retail has one
|
||||
/// <c>teleport_in_progress</c> flow). The portal route rides
|
||||
/// <see cref="AcknowledgePortalMaterialized"/> at its Place edge; the
|
||||
/// login route has no Place (the first-entry conductor already committed
|
||||
/// the canonical placement), so this acknowledges the same simulation
|
||||
/// release at the login pump's tunnel-hold-end edge. Without it the world
|
||||
/// stayed unavailable until <see cref="Complete"/> — the WorldFadeIn
|
||||
/// second presented an empty (void) world frame instead of retail's
|
||||
/// world-under-warp (2026-08-17 gate).
|
||||
/// </summary>
|
||||
public bool AcknowledgeLoginMaterialized(long generation)
|
||||
{
|
||||
if (generation == 0
|
||||
|| generation != _snapshot.Generation
|
||||
|| _snapshot.Kind != RuntimePortalKind.Login)
|
||||
{
|
||||
LogRejected(
|
||||
"materialized-login-mismatch",
|
||||
$"generation={generation} kind={_snapshot.Kind}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateActive(
|
||||
generation,
|
||||
_snapshot.DestinationCell,
|
||||
"materialized"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_snapshot.Materialized)
|
||||
return false;
|
||||
if (!_snapshot.Readiness.IsReady)
|
||||
{
|
||||
FailInvariant("materialized-before-ready", null);
|
||||
return false;
|
||||
}
|
||||
|
||||
// PortalMaterializationCount deliberately unchanged: it counts
|
||||
// Kind == Portal materializations only, exactly like the portal
|
||||
// acknowledgement's own conditional increment.
|
||||
_snapshot = _snapshot with
|
||||
{
|
||||
Materialized = true,
|
||||
WorldSimulationAvailable = true,
|
||||
};
|
||||
RequireHostStage(
|
||||
generation,
|
||||
RuntimeWorldHostAcknowledgementStage
|
||||
.SimulationReleaseProjected);
|
||||
Log("materialized", _snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AcknowledgeWorldViewportVisible(long generation)
|
||||
{
|
||||
if (!ValidateGeneration(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue