fix(world): remove non-retail portal exit fade
This commit is contained in:
parent
842bd89c16
commit
dded9e6b17
21 changed files with 919 additions and 347 deletions
|
|
@ -222,7 +222,6 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private AcDream.App.Rendering.PortalTunnelPresentation? _portalTunnel;
|
||||
private AcDream.App.Rendering.FadeOverlay? _fadeOverlay;
|
||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
|
|
@ -2588,8 +2587,7 @@ public sealed class GameWindow : IDisposable
|
|||
// T1: invisible portal depth writes (seal/punch) — retail
|
||||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
||||
_fadeOverlay = new AcDream.App.Rendering.FadeOverlay(_gl);
|
||||
_portalTunnel = AcDream.App.Rendering.PortalTunnelPresentation.TryCreate(
|
||||
_portalTunnel = AcDream.App.Rendering.PortalTunnelPresentation.CreateRequired(
|
||||
_gl,
|
||||
_dats!,
|
||||
_animLoader!,
|
||||
|
|
@ -2783,6 +2781,9 @@ public sealed class GameWindow : IDisposable
|
|||
private void ClearInboundEntityState()
|
||||
{
|
||||
EndMouseLookAndRestoreCursor();
|
||||
ResetTeleportTransitState(clearSession: true);
|
||||
if (_playerController is not null)
|
||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||
|
||||
// Session-scoped origin readiness. A reconnect's first logical player
|
||||
// CreateObject must initialize the render/streaming center anew;
|
||||
|
|
@ -6817,118 +6818,34 @@ public sealed class GameWindow : IDisposable
|
|||
entity.Rotation = rmState.Body.Orientation;
|
||||
}
|
||||
|
||||
// Phase B.3 / G.3a (#133): portal-space arrival detection.
|
||||
// Only runs for our own player character while in PortalSpace.
|
||||
if (_playerController is not null
|
||||
&& _playerController.State == AcDream.App.Input.PlayerState.PortalSpace
|
||||
&& update.Guid == _playerServerGuid)
|
||||
// F751 is only a notification gate; the accepted Position may arrive
|
||||
// before or after it. Canonical physics above always consumes the
|
||||
// packet first. The presentation coordinator exposes exactly one
|
||||
// sequence-correlated destination without reordering that state.
|
||||
if (timestampDisposition is AcDream.Core.Physics.PositionTimestampDisposition.Apply
|
||||
&& update.Guid == _playerServerGuid
|
||||
&& _teleportTransit.OfferDestination(
|
||||
update.TeleportSequence,
|
||||
timestamps.TeleportAdvanced,
|
||||
update,
|
||||
out var teleportDestination))
|
||||
{
|
||||
var oldPos = _playerController.Position;
|
||||
uint streamingOriginLandblockId =
|
||||
AcDream.App.Streaming.StreamingRegion.EncodeLandblockId(
|
||||
_liveCenterX, _liveCenterY);
|
||||
|
||||
// Retail preserves the full Position (objcell_id + Frame) through
|
||||
// SmartBox::TeleportPlayer @ 0x00453910. Dungeon EnvCells can have
|
||||
// negative local frame origins, so XYZ cannot identify the source
|
||||
// landblock (#215). Compare the controller's authoritative current
|
||||
// cell with the received destination cell instead.
|
||||
var landblockTransition =
|
||||
AcDream.App.Streaming.TeleportLandblockTransition.Classify(
|
||||
_playerController.CellId,
|
||||
p.LandblockId,
|
||||
streamingOriginLandblockId);
|
||||
int oldLbX = (int)((landblockTransition.SourceLandblockId >> 24) & 0xFFu);
|
||||
int oldLbY = (int)((landblockTransition.SourceLandblockId >> 16) & 0xFFu);
|
||||
bool differentLandblock = landblockTransition.CrossesLandblock;
|
||||
|
||||
Console.WriteLine(
|
||||
$"live: teleport arrival — old lb=({oldLbX},{oldLbY}) " +
|
||||
$"new lb=({lbX},{lbY}) dist={System.Numerics.Vector3.Distance(worldPos, oldPos):F1}");
|
||||
|
||||
System.Numerics.Vector3 newWorldPos;
|
||||
if (differentLandblock)
|
||||
{
|
||||
// #145: drop the stale SOURCE center landblock from the physics engine
|
||||
// BEFORE recentering. The destination loads at world-offset (0,0) (the new
|
||||
// center), but the prior center was ALSO loaded at offset (0,0) and its
|
||||
// offset is never re-based — so the two overlap, and the Z-agnostic outdoor
|
||||
// cell-snap (AdjustPosition / Resolve, iterating _landblocks) resolves the
|
||||
// player into the STALE source landblock (a dungeon's frame). That happens
|
||||
// for the arrival snap AND every per-frame resolve until the source finally
|
||||
// unloads, so outbound movement is sent in the dungeon's frame and the server
|
||||
// rejects every move as a failed transition (#145 / #138). Removing the stale
|
||||
// center here makes both resolves fall through to the server-authoritative
|
||||
// position (Resolve's NO-LANDBLOCK verbatim branch, :605) until the
|
||||
// destination streams in. Only the offset-(0,0) center can collide with the
|
||||
// destination-local position, so removing it alone is sufficient.
|
||||
_physicsEngine.RemoveLandblock(streamingOriginLandblockId);
|
||||
|
||||
// Recenter the streaming controller on the new landblock NOW (kick
|
||||
// off the dungeon load). After recentering, the destination is
|
||||
// (p.PositionX, p.PositionY, p.PositionZ) relative to the new origin.
|
||||
_liveCenterX = lbX;
|
||||
_liveCenterY = lbY;
|
||||
newWorldPos = new System.Numerics.Vector3(p.PositionX, p.PositionY, p.PositionZ);
|
||||
|
||||
// #135: pre-collapse on teleport into a sealed dungeon too — same
|
||||
// race as login. The destination isn't placed until it hydrates, so
|
||||
// without this NormalTick loads the full neighbor window during the
|
||||
// arrival hold. The PortalSpace observer branch (OnUpdate) keeps the
|
||||
// observer pinned to _liveCenterX/Y while held, so the stale frozen
|
||||
// player position can't drift the observer off the dungeon and re-expand.
|
||||
if (_streamingController is not null && IsSealedDungeonCell(p.LandblockId))
|
||||
_streamingController.PreCollapseToDungeon(lbX, lbY);
|
||||
else
|
||||
// Outdoor teleport: the render origin (_liveCenter) just moved. Drop ALL
|
||||
// resident terrain so overlapping blocks on a NEARBY jump re-bake at the
|
||||
// new origin instead of rendering shifted — the confirmed "terrain in the
|
||||
// sky" arcs (stale slots offset by exactly deltaLB×192). 2026-06-22.
|
||||
_streamingController?.ForceReloadWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
newWorldPos = worldPos;
|
||||
}
|
||||
|
||||
// Retail "pink-bubble" transit: do NOT snap here. Record the destination and
|
||||
// PRIORITIZE its landblock in streaming so it applies ahead of the per-frame
|
||||
// budget (residency in ~hundreds of ms, not 10-14s). The TAS — ticked per frame
|
||||
// in OnUpdate — holds the player in PortalSpace behind the fade until the
|
||||
// destination terrain is resident (TeleportWorldReady), then fires Place. While
|
||||
// held, no movement resolve runs, so the outbound cell frame can't corrupt.
|
||||
_pendingTeleportRot = rot;
|
||||
_pendingTeleportPos = newWorldPos;
|
||||
_pendingTeleportCell = p.LandblockId;
|
||||
_teleportHoldSeconds = 0f;
|
||||
_teleportForced = false;
|
||||
if (_streamingController is not null)
|
||||
{
|
||||
_streamingController.PriorityLandblockId =
|
||||
AcDream.App.Streaming.StreamingRegion.EncodeLandblockId(lbX, lbY);
|
||||
// Eager-apply the destination's IMMEDIATE SURROUNDINGS (the near ring), not
|
||||
// just the one landblock the player stands on — so they arrive in a loaded,
|
||||
// collidable world instead of a single landblock in the void.
|
||||
_streamingController.PriorityRadius = TeleportNearRingRadius;
|
||||
}
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"AIM", p.LandblockId,
|
||||
$"lb={lbX},{lbY} indoor={((p.LandblockId & 0xFFFFu) >= 0x0100u)} diffLb={differentLandblock}");
|
||||
AimTeleportDestination(teleportDestination);
|
||||
}
|
||||
}
|
||||
|
||||
// Retail teleport transit: the dormant 7-state TAS drives the fade cover, holds the
|
||||
// Retail teleport transit: the 7-state TAS drives portal/view-plane presentation, holds the
|
||||
// player in PortalSpace until the destination is resident (TeleportWorldReady), then
|
||||
// fires Place (materialize) and FireLoginComplete (regain control + ack the server).
|
||||
// Replaces the old TeleportArrivalController hold/place machine.
|
||||
private readonly AcDream.Core.World.TeleportAnimSequencer _teleportAnim = new();
|
||||
private readonly TeleportViewPlaneController _teleportViewPlane = new();
|
||||
private bool _teleportInProgress;
|
||||
private readonly AcDream.App.Streaming.TeleportTransitCoordinator<
|
||||
AcDream.Core.Net.WorldSession.EntityPositionUpdate> _teleportTransit = new();
|
||||
private System.Numerics.Vector3 _pendingTeleportPos;
|
||||
private uint _pendingTeleportCell;
|
||||
private float _teleportHoldSeconds; // wall-clock seconds waiting for residency (safety-net timeout)
|
||||
private bool _teleportForced; // true when the safety-net timeout force-places
|
||||
private float _teleportFadeAlpha; // 0 = clear, 1 = full black; consumed by the fade overlay
|
||||
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
||||
|
||||
// Loud safety net for a destination that never streams (worker crash / corrupt dat /
|
||||
|
|
@ -6939,13 +6856,133 @@ public sealed class GameWindow : IDisposable
|
|||
private const float TeleportMaxHoldSeconds = 10f;
|
||||
|
||||
// 2026-06-22: how many landblocks of the player's IMMEDIATE SURROUNDINGS to eager-apply
|
||||
// (and to hold the fade for) on a teleport. radius 1 = the 3×3 around the destination —
|
||||
// (and to hold portal space for) on a teleport. radius 1 = the 3×3 around the destination —
|
||||
// the player's own landblock + its 8 neighbours, ~576 m across — enough that they arrive
|
||||
// standing on loaded ground with wall collision and a non-empty immediate view. The far
|
||||
// ring (out to the streaming window) still drains at the per-frame budget after the fade
|
||||
// lifts. Bigger = a more complete arrival but a longer (still hidden) loading fade.
|
||||
// ring (out to the streaming window) still drains at the per-frame budget after portal
|
||||
// space exits. Bigger = a more complete arrival but a longer portal-space hold.
|
||||
private const int TeleportNearRingRadius = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Bind a sequence-correlated teleport Position to presentation and
|
||||
/// asynchronous streaming. Canonical physics has already accepted this
|
||||
/// packet; this method only establishes the destination viewport lifetime.
|
||||
/// </summary>
|
||||
private void AimTeleportDestination(
|
||||
AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
var playerController = _playerController
|
||||
?? throw new InvalidOperationException(
|
||||
"A teleport destination was accepted before the local player controller existed.");
|
||||
var p = update.Position;
|
||||
int lbX = (int)((p.LandblockId >> 24) & 0xFFu);
|
||||
int lbY = (int)((p.LandblockId >> 16) & 0xFFu);
|
||||
uint streamingOriginLandblockId =
|
||||
AcDream.App.Streaming.StreamingRegion.EncodeLandblockId(
|
||||
_liveCenterX, _liveCenterY);
|
||||
var origin = new System.Numerics.Vector3(
|
||||
(lbX - _liveCenterX) * 192f,
|
||||
(lbY - _liveCenterY) * 192f,
|
||||
0f);
|
||||
var worldPos = new System.Numerics.Vector3(
|
||||
p.PositionX, p.PositionY, p.PositionZ) + origin;
|
||||
|
||||
// Retail preserves the full Position (objcell_id + Frame) through
|
||||
// SmartBox::TeleportPlayer @ 0x00453910. Dungeon EnvCells can have
|
||||
// negative local frame origins, so XYZ cannot identify the source
|
||||
// landblock (#215). Player crossing and streaming recentering are
|
||||
// separate while a prior destination is aimed but not yet placed.
|
||||
var landblockTransition =
|
||||
AcDream.App.Streaming.TeleportLandblockTransition.Classify(
|
||||
playerController.CellId,
|
||||
p.LandblockId,
|
||||
streamingOriginLandblockId);
|
||||
int oldLbX = (int)((landblockTransition.SourceLandblockId >> 24) & 0xFFu);
|
||||
int oldLbY = (int)((landblockTransition.SourceLandblockId >> 16) & 0xFFu);
|
||||
bool playerCrossesLandblock = landblockTransition.CrossesLandblock;
|
||||
bool streamingCenterChanges = landblockTransition.ChangesStreamingCenter;
|
||||
|
||||
Console.WriteLine(
|
||||
$"live: teleport arrival — old lb=({oldLbX},{oldLbY}) " +
|
||||
$"new lb=({lbX},{lbY}) dist={System.Numerics.Vector3.Distance(worldPos, playerController.Position):F1}");
|
||||
|
||||
System.Numerics.Vector3 newWorldPos;
|
||||
if (streamingCenterChanges)
|
||||
{
|
||||
// #145: drop the stale STREAMING center landblock from the physics engine
|
||||
// BEFORE recentering. The destination loads at world-offset (0,0) (the new
|
||||
// center), but the prior center was ALSO loaded at offset (0,0) and its
|
||||
// offset is never re-based — so the two overlap, and the Z-agnostic outdoor
|
||||
// cell-snap resolves the player into the stale center. On a replacement this
|
||||
// can be the abandoned first destination, not the unplaced player's source.
|
||||
_physicsEngine.RemoveLandblock(streamingOriginLandblockId);
|
||||
|
||||
_liveCenterX = lbX;
|
||||
_liveCenterY = lbY;
|
||||
newWorldPos = new System.Numerics.Vector3(
|
||||
p.PositionX, p.PositionY, p.PositionZ);
|
||||
|
||||
if (_streamingController is not null && IsSealedDungeonCell(p.LandblockId))
|
||||
_streamingController.PreCollapseToDungeon(lbX, lbY);
|
||||
else
|
||||
_streamingController?.ForceReloadWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
newWorldPos = worldPos;
|
||||
}
|
||||
|
||||
// Do not snap here. The TAS holds portal space until the destination
|
||||
// near ring is resident, then emits Place exactly once.
|
||||
_pendingTeleportRot = new System.Numerics.Quaternion(
|
||||
p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
|
||||
_pendingTeleportPos = newWorldPos;
|
||||
_pendingTeleportCell = p.LandblockId;
|
||||
_teleportHoldSeconds = 0f;
|
||||
_teleportForced = false;
|
||||
if (_streamingController is not null)
|
||||
{
|
||||
_streamingController.PriorityLandblockId =
|
||||
AcDream.App.Streaming.StreamingRegion.EncodeLandblockId(lbX, lbY);
|
||||
_streamingController.PriorityRadius = TeleportNearRingRadius;
|
||||
}
|
||||
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"AIM", p.LandblockId,
|
||||
$"seq={update.TeleportSequence} lb={lbX},{lbY} " +
|
||||
$"indoor={((p.LandblockId & 0xFFFFu) >= 0x0100u)} " +
|
||||
$"playerCross={playerCrossesLandblock} centerChange={streamingCenterChanges}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End one logical teleport lifetime. This is the single symmetry seam
|
||||
/// used by successful completion, replacement by a newer sequence, and
|
||||
/// session teardown, so presentation state and destination state cannot
|
||||
/// leak independently.
|
||||
/// </summary>
|
||||
private void ResetTeleportTransitState(bool clearSession = false)
|
||||
{
|
||||
if (clearSession)
|
||||
_teleportTransit.ClearSession();
|
||||
else
|
||||
_teleportTransit.EndActive();
|
||||
_pendingTeleportPos = default;
|
||||
_pendingTeleportCell = 0u;
|
||||
_pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
||||
_teleportHoldSeconds = 0f;
|
||||
_teleportForced = false;
|
||||
|
||||
_teleportAnim.Reset();
|
||||
_teleportViewPlane.Reset();
|
||||
_portalTunnel?.Exit();
|
||||
|
||||
if (_streamingController is not null)
|
||||
{
|
||||
_streamingController.PriorityLandblockId = 0u;
|
||||
_streamingController.PriorityRadius = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
||||
// body's cell-relative CellPosition. This is the ONE place the streaming center
|
||||
// (_liveCenter) is allowed to touch the physics frame — at the placement seam,
|
||||
|
|
@ -6967,7 +7004,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// can materialize? Indoor (sealed dungeon / building interior) gates on the EnvCell
|
||||
/// struct hydrating (#135); outdoor gates on the destination's NEAR RING (the 3×3 around
|
||||
/// the player, <see cref="TeleportNearRingRadius"/>) being registered — not just the
|
||||
/// single destination landblock — so the fade only lifts onto a loaded, collidable world
|
||||
/// single destination landblock — so portal space exits onto a loaded, collidable world
|
||||
/// (the player's cell-walk can root into neighbour cells; no walk-through-walls and no
|
||||
/// "only one landblock loaded"). The streaming controller priority-applies that same ring,
|
||||
/// so it flips fast. An impossible claim (indoor cell id outside the dat's NumCells)
|
||||
|
|
@ -7032,35 +7069,79 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"PLACED", resolved.CellId, $"forced={forced}");
|
||||
// Do NOT flip to InWorld or send LoginComplete here — the player materializes BEHIND
|
||||
// the fade and stays input-frozen (PortalSpace) until the TAS fades the world back in
|
||||
// and fires FireLoginComplete (which regains control + acks the server). This is the
|
||||
// Do NOT flip to InWorld or send LoginComplete here — the player materializes while
|
||||
// the portal viewport is active and stays input-frozen (PortalSpace) until the TAS
|
||||
// restores the destination projection and fires FireLoginComplete. This is the
|
||||
// retail "pop out the other side" sequence.
|
||||
Console.WriteLine($"live: teleport materialized — snapped to {snappedPos} cell=0x{resolved.CellId:X8}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase B.3: fires when the server sends a PlayerTeleport (0xF751). Freeze movement
|
||||
/// input (PortalSpace) and begin the retail fade transit. The per-frame TAS tick holds
|
||||
/// the player behind the fade until the destination is resident, then materializes them
|
||||
/// (Place) and, after the world fades back in, regains control + acks the server
|
||||
/// input (PortalSpace) and begin retail portal transit. The per-frame TAS tick holds
|
||||
/// the player behind the portal viewport until the destination is resident, then materializes
|
||||
/// them (Place) and, after the destination view-plane restores, regains control + acks the server
|
||||
/// (FireLoginComplete).
|
||||
/// </summary>
|
||||
private void OnTeleportStarted(uint sequence)
|
||||
{
|
||||
if (!_liveEntities!.IsFreshTeleportStart(_playerServerGuid, (ushort)sequence))
|
||||
ushort teleportSequence = (ushort)sequence;
|
||||
if (!_liveEntities!.IsFreshTeleportStart(_playerServerGuid, teleportSequence)
|
||||
|| !_teleportTransit.CanBegin(teleportSequence))
|
||||
return;
|
||||
|
||||
EndMouseLookAndRestoreCursor();
|
||||
if (_playerController is not null)
|
||||
_playerController.State = AcDream.App.Input.PlayerState.PortalSpace;
|
||||
_teleportInProgress = true;
|
||||
// A fresh sequence is a new logical transit. Discard the prior
|
||||
// destination and presentation before this sequence can observe them;
|
||||
// its destination PositionUpdate may arrive on a later network tick.
|
||||
ResetTeleportTransitState();
|
||||
_teleportTransit.QueueStart(teleportSequence);
|
||||
TryActivatePendingTeleportPresentation();
|
||||
Console.WriteLine($"live: teleport queued (seq={sequence})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converge a queued F751 notification with the ordinary live-player
|
||||
/// projection lifecycle. Usually this activates synchronously. If a
|
||||
/// session transition has not materialized the player projection yet, the
|
||||
/// notification and any correlated Position remain queued until it does.
|
||||
/// </summary>
|
||||
private void TryActivatePendingTeleportPresentation()
|
||||
{
|
||||
if (!_teleportTransit.HasPendingStart)
|
||||
return;
|
||||
|
||||
// Retail has no detached fly/orbit gameplay mode. If acdream's live
|
||||
// developer camera currently owns the view, restore the existing
|
||||
// player-mode lifecycle before transit so placement still has the
|
||||
// canonical local physics controller and chase-camera handoff.
|
||||
var playerController = _playerController;
|
||||
if (playerController is null)
|
||||
{
|
||||
if (!_entitiesByServerGuid.ContainsKey(_playerServerGuid))
|
||||
return;
|
||||
|
||||
_playerMode = true;
|
||||
if (!EnterPlayerModeNow(loggingTag: "teleport"))
|
||||
{
|
||||
_playerMode = false;
|
||||
return;
|
||||
}
|
||||
playerController = _playerController;
|
||||
}
|
||||
if (playerController is null)
|
||||
return;
|
||||
playerController.State = AcDream.App.Input.PlayerState.PortalSpace;
|
||||
_teleportHoldSeconds = 0f;
|
||||
_teleportForced = false;
|
||||
_teleportViewPlane.Begin(
|
||||
_cameraController?.Active.Projection ?? System.Numerics.Matrix4x4.Identity);
|
||||
_teleportAnim.Begin(AcDream.Core.World.TeleportEntryKind.Portal);
|
||||
Console.WriteLine($"live: teleport started (seq={sequence})");
|
||||
bool hasBufferedDestination = _teleportTransit.Activate(
|
||||
out var bufferedDestination);
|
||||
if (hasBufferedDestination)
|
||||
AimTeleportDestination(bufferedDestination);
|
||||
Console.WriteLine($"live: teleport presentation started (seq={_teleportTransit.Sequence})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -8881,14 +8962,19 @@ public sealed class GameWindow : IDisposable
|
|||
// Step 2: routed through the controller; functionally identical.
|
||||
_liveSessionController?.Tick();
|
||||
|
||||
// Usually F751 activates immediately. This no-op convergence check
|
||||
// covers the session edge where the canonical player exists before
|
||||
// its normal live projection/controller has materialized.
|
||||
TryActivatePendingTeleportPresentation();
|
||||
|
||||
// Retail teleport transit. Runs AFTER streaming (which priority-applies the
|
||||
// destination landblock this frame) and the live-session drain, so a destination
|
||||
// that became resident this frame materializes the same frame. The TAS holds at
|
||||
// Tunnel until worldReady, fires Place (materialize, hidden), then after the min-
|
||||
// continue + fades fires FireLoginComplete (regain control + ack). PortalTunnelPresentation
|
||||
// replaces the world viewport through the tunnel-family states; FadeOverlay transitions
|
||||
// between the two viewports below the retained UI.
|
||||
if (_teleportInProgress)
|
||||
// continue + view-plane transitions fires FireLoginComplete (regain control + ack).
|
||||
// PortalTunnelPresentation replaces the world viewport through the tunnel-family states;
|
||||
// retail switches the two 3-D viewports directly, below the retained UI.
|
||||
if (_teleportTransit.IsActive)
|
||||
{
|
||||
bool haveDest = _pendingTeleportCell != 0u;
|
||||
bool ready = haveDest && TeleportWorldReady(_pendingTeleportCell);
|
||||
|
|
@ -8905,12 +8991,6 @@ public sealed class GameWindow : IDisposable
|
|||
int tunnelFrame = _portalTunnel?.CurrentAnimationFrame ?? 0;
|
||||
var (snap, evts) = _teleportAnim.Tick((float)dt, ready, tunnelFrame);
|
||||
_teleportViewPlane.Update(snap);
|
||||
// The fade is a viewport transition below retained UI. If the
|
||||
// installed DATs are corrupt and the retail portal scene could
|
||||
// not be built, retain an opaque world cover during transit.
|
||||
_teleportFadeAlpha = _portalTunnel is null && snap.ShowTunnel
|
||||
? 1f
|
||||
: snap.FadeAlpha;
|
||||
|
||||
foreach (var e in evts)
|
||||
{
|
||||
|
|
@ -8937,10 +9017,7 @@ public sealed class GameWindow : IDisposable
|
|||
// each portal transition.
|
||||
_liveSession?.SendGameAction(
|
||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||
_teleportInProgress = false;
|
||||
_pendingTeleportCell = 0u;
|
||||
_teleportViewPlane.Reset();
|
||||
_teleportFadeAlpha = 0f;
|
||||
ResetTeleportTransitState();
|
||||
break;
|
||||
default:
|
||||
// The retained audio engine does not yet own the
|
||||
|
|
@ -9575,8 +9652,9 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
if (_cameraController is not null)
|
||||
{
|
||||
var camera = _cameraController.Active;
|
||||
var worldProjection = _teleportViewPlane.Apply(camera.Projection);
|
||||
var activeCamera = _cameraController.Active;
|
||||
var camera = _teleportViewPlane.ApplyTo(activeCamera);
|
||||
var worldProjection = camera.Projection;
|
||||
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * worldProjection);
|
||||
|
||||
// Extract camera world position from the inverse of the view
|
||||
|
|
@ -10489,7 +10567,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
// Retail gmSmartBoxUI swaps the world SmartBox viewport for a
|
||||
// CreatureMode portal-space viewport. This replacement scene and
|
||||
// its black transition draw BEFORE retained UI, so chat, windows,
|
||||
// its projection transition draws BEFORE retained UI, so chat, windows,
|
||||
// cursor, and toolbar remain visible and interactive in transit.
|
||||
var portalProjection = _teleportViewPlane.Apply(
|
||||
_cameraController!.Active.Projection);
|
||||
|
|
@ -10497,7 +10575,6 @@ public sealed class GameWindow : IDisposable
|
|||
_window!.Size.X,
|
||||
_window.Size.Y,
|
||||
portalProjection);
|
||||
_fadeOverlay?.Draw(_teleportFadeAlpha);
|
||||
|
||||
// Phase D.2b Sub-phase C Slice 2 — paperdoll 3-D doll: render the re-dressed player clone into the
|
||||
// viewport's off-screen texture BEFORE the 2-D UI pass blits it, but only when the inventory window
|
||||
|
|
@ -14693,7 +14770,6 @@ public sealed class GameWindow : IDisposable
|
|||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_portalDepthMask?.Dispose(); // T1
|
||||
_fadeOverlay?.Dispose();
|
||||
_clipFrame?.Dispose(); // Phase U.3
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_particleRenderer?.Dispose(); // releases particle mesh refs before WB
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue