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
|
|
@ -1,119 +0,0 @@
|
|||
using System;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Fullscreen black quad at a given alpha for retail's world/portal viewport
|
||||
/// transitions. It is drawn after the active 3-D viewport but before retained
|
||||
/// UI, matching <c>gmSmartBoxUI::UseTime</c>: the world or portal scene fades
|
||||
/// while chat, toolbar, windows, and cursor remain fully visible.
|
||||
///
|
||||
/// <para>Draws in NDC, so it needs no view/projection. Self-contained GL state
|
||||
/// (feedback_render_self_contained_gl_state): sets blend + disables depth, restores the
|
||||
/// frame-global convention (depth test/write on) on exit.</para>
|
||||
/// </summary>
|
||||
public sealed class FadeOverlay : IDisposable
|
||||
{
|
||||
private const string VertSrc = @"#version 430 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }";
|
||||
|
||||
private const string FragSrc = @"#version 430 core
|
||||
uniform float uAlpha;
|
||||
out vec4 FragColor;
|
||||
void main() { FragColor = vec4(0.0, 0.0, 0.0, uAlpha); }";
|
||||
|
||||
private readonly GL _gl;
|
||||
private readonly uint _program;
|
||||
private readonly uint _vao;
|
||||
private readonly uint _vbo;
|
||||
private readonly int _locAlpha;
|
||||
|
||||
// Two triangles covering the whole NDC viewport.
|
||||
private static readonly float[] QuadVerts =
|
||||
{
|
||||
-1f, -1f, 1f, -1f, 1f, 1f,
|
||||
-1f, -1f, 1f, 1f, -1f, 1f,
|
||||
};
|
||||
|
||||
public FadeOverlay(GL gl)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
|
||||
uint vs = Compile(ShaderType.VertexShader, VertSrc);
|
||||
uint fs = Compile(ShaderType.FragmentShader, FragSrc);
|
||||
_program = _gl.CreateProgram();
|
||||
_gl.AttachShader(_program, vs);
|
||||
_gl.AttachShader(_program, fs);
|
||||
_gl.LinkProgram(_program);
|
||||
_gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int linked);
|
||||
if (linked == 0)
|
||||
throw new InvalidOperationException($"FadeOverlay link failed: {_gl.GetProgramInfoLog(_program)}");
|
||||
_gl.DeleteShader(vs);
|
||||
_gl.DeleteShader(fs);
|
||||
|
||||
_locAlpha = _gl.GetUniformLocation(_program, "uAlpha");
|
||||
|
||||
_vao = _gl.GenVertexArray();
|
||||
_vbo = _gl.GenBuffer();
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
unsafe
|
||||
{
|
||||
fixed (float* v = QuadVerts)
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(QuadVerts.Length * sizeof(float)), v, BufferUsageARB.StaticDraw);
|
||||
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 2 * sizeof(float), (void*)0);
|
||||
}
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
}
|
||||
|
||||
private uint Compile(ShaderType type, string src)
|
||||
{
|
||||
uint s = _gl.CreateShader(type);
|
||||
_gl.ShaderSource(s, src);
|
||||
_gl.CompileShader(s);
|
||||
_gl.GetShader(s, ShaderParameterName.CompileStatus, out int ok);
|
||||
if (ok == 0)
|
||||
throw new InvalidOperationException($"FadeOverlay {type} compile failed: {_gl.GetShaderInfoLog(s)}");
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw the fullscreen black cover at <paramref name="alpha"/> (0 = clear → no-op,
|
||||
/// 1 = opaque). Call after the active 3-D viewport and before retained UI.
|
||||
/// </summary>
|
||||
public void Draw(float alpha)
|
||||
{
|
||||
if (alpha <= 0f) return;
|
||||
alpha = Math.Clamp(alpha, 0f, 1f);
|
||||
|
||||
// ---- set state (everything this draw depends on) ----
|
||||
_gl.Disable(EnableCap.DepthTest);
|
||||
_gl.DepthMask(false);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
|
||||
_gl.UseProgram(_program);
|
||||
_gl.Uniform1(_locAlpha, alpha);
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, 6);
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.UseProgram(0);
|
||||
|
||||
// ---- restore the frame-global convention ----
|
||||
_gl.DepthMask(true);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteProgram(_program);
|
||||
_gl.DeleteVertexArray(_vao);
|
||||
_gl.DeleteBuffer(_vbo);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -103,10 +103,11 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Resolve retail's enum-mapped portal Setup and animation through the
|
||||
/// installed DATs. Missing assets fail closed with an actionable message;
|
||||
/// no substitute tunnel is fabricated.
|
||||
/// installed DATs. Retail dereferences this scene unconditionally; missing
|
||||
/// required assets therefore fail startup with an actionable diagnostic.
|
||||
/// No substitute tunnel is fabricated.
|
||||
/// </summary>
|
||||
public static PortalTunnelPresentation? TryCreate(
|
||||
public static PortalTunnelPresentation CreateRequired(
|
||||
GL gl,
|
||||
DatCollection dats,
|
||||
IAnimationLoader animationLoader,
|
||||
|
|
@ -132,21 +133,18 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
? null
|
||||
: animationLoader.LoadAnimation(animationDid);
|
||||
|
||||
if (setup is null || animation is null)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[portal-space] retail DAT assets unavailable: "
|
||||
+ $"setup=0x{setupDid:X8} ({(setup is null ? "missing" : "ok")}), "
|
||||
+ $"animation=0x{animationDid:X8} ({(animation is null ? "missing" : "ok")})");
|
||||
return null;
|
||||
}
|
||||
EnsureRequiredAssets(
|
||||
setupDid,
|
||||
setup is not null,
|
||||
animationDid,
|
||||
animation is not null);
|
||||
|
||||
return new PortalTunnelPresentation(
|
||||
gl,
|
||||
dispatcher,
|
||||
lightUbo,
|
||||
meshAdapter,
|
||||
setup,
|
||||
setup!,
|
||||
setupDid,
|
||||
animationDid,
|
||||
animationLoader,
|
||||
|
|
@ -155,6 +153,21 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
displayNotice);
|
||||
}
|
||||
|
||||
internal static void EnsureRequiredAssets(
|
||||
uint setupDid,
|
||||
bool setupLoaded,
|
||||
uint animationDid,
|
||||
bool animationLoaded)
|
||||
{
|
||||
if (setupLoaded && animationLoaded)
|
||||
return;
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"[portal-space] required retail DAT assets unavailable: "
|
||||
+ $"setup=0x{setupDid:X8} ({(setupLoaded ? "ok" : "missing")}), "
|
||||
+ $"animation=0x{animationDid:X8} ({(animationLoaded ? "ok" : "missing")})");
|
||||
}
|
||||
|
||||
public bool IsVisible => _visible;
|
||||
public int CurrentAnimationFrame => _sequence.GetCurrFrameNumber();
|
||||
public uint SetupDid => _setupDid;
|
||||
|
|
@ -210,7 +223,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Replace the already-rendered world viewport with retail portal space.
|
||||
/// The caller then draws the fade and retained UI above this pass.
|
||||
/// The caller then draws retained UI above this pass.
|
||||
/// </summary>
|
||||
public void Draw(int width, int height, Matrix4x4 smartBoxProjection)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,14 +8,16 @@ namespace AcDream.App.Rendering;
|
|||
/// (<c>0x004D6E30</c>) eases SmartBox's view-plane distance between the
|
||||
/// active game's value and <c>TRANSITION_VIEW_PLANE_DISTANCE = 0.001</c>.
|
||||
/// <c>Render::set_vdst</c> (<c>0x0054B240</c>) converts that distance back to
|
||||
/// FOV and adjusts the near plane. This is the wide projection warp visible
|
||||
/// while the destination world emerges from black.
|
||||
/// FOV and adjusts the near plane. At the exit edge retail swaps directly
|
||||
/// from portal space to the destination world at the transition projection;
|
||||
/// there is no black-alpha compositor between them.
|
||||
/// </summary>
|
||||
public sealed class TeleportViewPlaneController
|
||||
{
|
||||
public const float TransitionViewPlaneDistance = 0.001f;
|
||||
|
||||
private float _gameViewPlaneDistance = 1f;
|
||||
private readonly ProjectionOverrideCamera _projectionCamera = new();
|
||||
|
||||
public bool Enabled { get; private set; }
|
||||
public float CurrentViewPlaneDistance { get; private set; } = 1f;
|
||||
|
|
@ -38,20 +40,42 @@ public sealed class TeleportViewPlaneController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the four retail fade-state branches. Their projection blend is
|
||||
/// numerically the same table-driven level as the black fade: normal to
|
||||
/// 0.001 on fade-out, and 0.001 back to normal on fade-in.
|
||||
/// Apply retail's view-plane state machine. Here "fade" is retail's state
|
||||
/// name for a projection transition: normal to 0.001 on fade-out, and
|
||||
/// 0.001 back to normal on fade-in. A logout transition retains the
|
||||
/// captured override during stable Tunnel; <c>EndTeleportAnimation</c>
|
||||
/// (<c>0x004D65D5</c>) releases it when entering TunnelContinue.
|
||||
/// </summary>
|
||||
public void Update(TeleportAnimSnapshot snapshot)
|
||||
{
|
||||
Enabled = snapshot.State is TeleportAnimState.WorldFadeOut
|
||||
or TeleportAnimState.TunnelFadeIn
|
||||
or TeleportAnimState.TunnelFadeOut
|
||||
or TeleportAnimState.WorldFadeIn;
|
||||
switch (snapshot.State)
|
||||
{
|
||||
case TeleportAnimState.WorldFadeOut:
|
||||
case TeleportAnimState.TunnelFadeIn:
|
||||
case TeleportAnimState.TunnelFadeOut:
|
||||
case TeleportAnimState.WorldFadeIn:
|
||||
Enabled = true;
|
||||
CurrentViewPlaneDistance = Lerp(
|
||||
_gameViewPlaneDistance,
|
||||
TransitionViewPlaneDistance,
|
||||
snapshot.ViewPlaneBlend);
|
||||
break;
|
||||
|
||||
CurrentViewPlaneDistance = Enabled
|
||||
? Lerp(_gameViewPlaneDistance, TransitionViewPlaneDistance, snapshot.FadeAlpha)
|
||||
: _gameViewPlaneDistance;
|
||||
case TeleportAnimState.Tunnel:
|
||||
// Retail preserves the captured FOV override through the
|
||||
// logout path's stable tunnel. A normal portal begins in
|
||||
// Tunnel with no override, so its disabled state is retained.
|
||||
if (Enabled)
|
||||
CurrentViewPlaneDistance = _gameViewPlaneDistance;
|
||||
break;
|
||||
|
||||
case TeleportAnimState.TunnelContinue:
|
||||
case TeleportAnimState.Off:
|
||||
default:
|
||||
Enabled = false;
|
||||
CurrentViewPlaneDistance = _gameViewPlaneDistance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
|
|
@ -93,6 +117,39 @@ public sealed class TeleportViewPlaneController
|
|||
return Matrix4x4.CreatePerspectiveFieldOfView(fov, aspect, near, far);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decorate the active camera with the same projection returned by
|
||||
/// <see cref="Apply(Matrix4x4)"/>. Retail's <c>Render::set_vdst</c> is
|
||||
/// global to every 3-D draw; callers must pass this returned camera to
|
||||
/// terrain, meshes, particles, sky, weather, and portal-cell rendering so
|
||||
/// rasterization and frustum culling cannot use different projections.
|
||||
/// </summary>
|
||||
public ICamera ApplyTo(ICamera baseCamera)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(baseCamera);
|
||||
_projectionCamera.Update(baseCamera, Apply(baseCamera.Projection));
|
||||
return _projectionCamera;
|
||||
}
|
||||
|
||||
private static float Lerp(float from, float to, float amount) =>
|
||||
from + (to - from) * Math.Clamp(amount, 0f, 1f);
|
||||
|
||||
private sealed class ProjectionOverrideCamera : ICamera
|
||||
{
|
||||
private ICamera _source = null!;
|
||||
|
||||
public Matrix4x4 View => _source.View;
|
||||
public Matrix4x4 Projection { get; private set; } = Matrix4x4.Identity;
|
||||
public float Aspect
|
||||
{
|
||||
get => _source.Aspect;
|
||||
set => _source.Aspect = value;
|
||||
}
|
||||
|
||||
public void Update(ICamera source, Matrix4x4 projection)
|
||||
{
|
||||
_source = source;
|
||||
Projection = projection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue