Join destination scheduling to the canonical reveal generation, protect its share across every typed frame-budget dimension, and prevent stale work from clearing a replacement reservation. Remove forced incomplete materialization and project retail's centered portal wait cue while the authored tunnel remains active. Tests: Release build clean; 91 focused reservation/reveal tests; full solution 8,158 passed, 5 skipped. Co-authored-by: Codex <noreply@openai.com>
745 lines
26 KiB
C#
745 lines
26 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Net;
|
|
using AcDream.App.Physics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Update;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
internal interface ILocalPlayerTeleportNetworkSink
|
|
{
|
|
void OnTeleportStarted(uint sequence);
|
|
|
|
void OfferDestination(
|
|
WorldSession.EntityPositionUpdate update,
|
|
bool teleportTimestampAdvanced);
|
|
|
|
void ResetSession();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Construction-order bridge for the inbound session. It carries no teleport
|
|
/// state: after binding, every packet and reset is forwarded to the one
|
|
/// <see cref="LocalPlayerTeleportController"/> owner.
|
|
/// </summary>
|
|
internal sealed class DeferredLocalPlayerTeleportNetworkSink
|
|
: ILocalPlayerTeleportNetworkSink
|
|
{
|
|
private ILocalPlayerTeleportNetworkSink? _inner;
|
|
|
|
public void Bind(ILocalPlayerTeleportNetworkSink inner)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(inner);
|
|
if (Interlocked.CompareExchange(ref _inner, inner, null) is not null)
|
|
throw new InvalidOperationException("The local teleport sink is already bound.");
|
|
}
|
|
|
|
public IDisposable BindOwned(ILocalPlayerTeleportNetworkSink inner)
|
|
{
|
|
Bind(inner);
|
|
return new Binding(this, inner);
|
|
}
|
|
|
|
private void Unbind(ILocalPlayerTeleportNetworkSink expected)
|
|
{
|
|
_ = Interlocked.CompareExchange(ref _inner, null, expected);
|
|
}
|
|
|
|
public void OnTeleportStarted(uint sequence) => Required().OnTeleportStarted(sequence);
|
|
|
|
public void OfferDestination(
|
|
WorldSession.EntityPositionUpdate update,
|
|
bool teleportTimestampAdvanced) =>
|
|
Required().OfferDestination(update, teleportTimestampAdvanced);
|
|
|
|
public void ResetSession() => Required().ResetSession();
|
|
|
|
private ILocalPlayerTeleportNetworkSink Required() =>
|
|
_inner ?? throw new InvalidOperationException(
|
|
"The local teleport sink was used before composition completed.");
|
|
|
|
private sealed class Binding : IDisposable
|
|
{
|
|
private DeferredLocalPlayerTeleportNetworkSink? _owner;
|
|
private readonly ILocalPlayerTeleportNetworkSink _expected;
|
|
|
|
public Binding(
|
|
DeferredLocalPlayerTeleportNetworkSink owner,
|
|
ILocalPlayerTeleportNetworkSink expected)
|
|
{
|
|
_owner = owner;
|
|
_expected = expected;
|
|
}
|
|
|
|
public void Dispose() =>
|
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
|
}
|
|
}
|
|
|
|
internal interface ILocalPlayerTeleportInputLifetime
|
|
{
|
|
void EndMouseLook();
|
|
}
|
|
|
|
internal interface ILocalPlayerTeleportModeOperations
|
|
{
|
|
PlayerMovementController? Controller { get; }
|
|
Matrix4x4 Projection { get; }
|
|
bool TryEnterPortalSpace();
|
|
void EnterWorld();
|
|
}
|
|
|
|
internal interface ILocalPlayerTeleportAuthority
|
|
{
|
|
bool IsFreshStart(ushort sequence);
|
|
}
|
|
|
|
internal sealed class LiveLocalPlayerTeleportAuthority
|
|
: ILocalPlayerTeleportAuthority
|
|
{
|
|
private readonly LiveEntityRuntime _liveEntities;
|
|
private readonly ILocalPlayerIdentitySource _identity;
|
|
|
|
public LiveLocalPlayerTeleportAuthority(
|
|
LiveEntityRuntime liveEntities,
|
|
ILocalPlayerIdentitySource identity)
|
|
{
|
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
|
}
|
|
|
|
public bool IsFreshStart(ushort sequence) =>
|
|
_liveEntities.IsFreshTeleportStart(_identity.ServerGuid, sequence);
|
|
}
|
|
|
|
internal interface ILocalPlayerTeleportStreamingOperations
|
|
{
|
|
int CenterX { get; }
|
|
int CenterY { get; }
|
|
bool IsRecenterPending { get; }
|
|
bool BeginRecenter(int x, int y, bool isSealedDungeon);
|
|
bool ResetRecenter(bool sessionEnding);
|
|
bool IsSealedDungeon(uint cellId);
|
|
}
|
|
|
|
internal sealed class LocalPlayerTeleportStreamingOperations
|
|
: ILocalPlayerTeleportStreamingOperations
|
|
{
|
|
private readonly LiveWorldOriginState _origin;
|
|
private readonly StreamingOriginRecenterCoordinator _recenter;
|
|
private readonly StreamingController _streaming;
|
|
private readonly ISealedDungeonCellClassifier _sealedDungeonCells;
|
|
|
|
public LocalPlayerTeleportStreamingOperations(
|
|
LiveWorldOriginState origin,
|
|
StreamingOriginRecenterCoordinator recenter,
|
|
StreamingController streaming,
|
|
ISealedDungeonCellClassifier sealedDungeonCells)
|
|
{
|
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
|
_recenter = recenter ?? throw new ArgumentNullException(nameof(recenter));
|
|
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
|
_sealedDungeonCells = sealedDungeonCells
|
|
?? throw new ArgumentNullException(nameof(sealedDungeonCells));
|
|
}
|
|
|
|
public int CenterX => _origin.CenterX;
|
|
public int CenterY => _origin.CenterY;
|
|
public bool IsRecenterPending => _recenter.IsPending;
|
|
|
|
public bool BeginRecenter(int x, int y, bool isSealedDungeon) =>
|
|
_recenter.Begin(x, y, isSealedDungeon);
|
|
|
|
public bool ResetRecenter(bool sessionEnding) =>
|
|
_recenter.Reset(sessionEnding);
|
|
|
|
public bool IsSealedDungeon(uint cellId) =>
|
|
_sealedDungeonCells.IsSealedDungeon(cellId);
|
|
|
|
}
|
|
|
|
internal interface ILocalPlayerTeleportPlacement
|
|
{
|
|
void Place(Vector3 position, uint cellId, Quaternion rotation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits the local player's deferred portal arrival. It owns the exact
|
|
/// Place -> root/controller/camera mutation -> spatial reconcile edge.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlacement
|
|
{
|
|
private readonly PhysicsEngine _physics;
|
|
private readonly LiveEntityRuntime _liveEntities;
|
|
private readonly ILocalPlayerIdentitySource _identity;
|
|
private readonly ILocalPlayerControllerSource _controller;
|
|
private readonly ILocalPlayerPhysicsHostSource _host;
|
|
private readonly ChaseCameraInputState _cameras;
|
|
private readonly LiveWorldOriginState _origin;
|
|
private readonly ILiveSpatialReconcilePhase _spatial;
|
|
|
|
public LocalPlayerTeleportPlacement(
|
|
PhysicsEngine physics,
|
|
LiveEntityRuntime liveEntities,
|
|
ILocalPlayerIdentitySource identity,
|
|
ILocalPlayerControllerSource controller,
|
|
ILocalPlayerPhysicsHostSource host,
|
|
ChaseCameraInputState cameras,
|
|
LiveWorldOriginState origin,
|
|
ILiveSpatialReconcilePhase spatial)
|
|
{
|
|
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
|
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
|
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
|
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
|
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
|
}
|
|
|
|
public void Place(Vector3 position, uint cellId, Quaternion rotation)
|
|
{
|
|
PlayerMovementController controller = _controller.Controller
|
|
?? throw new InvalidOperationException(
|
|
"Teleport Place ran without the local player controller.");
|
|
var resolved = _physics.Resolve(
|
|
position,
|
|
cellId,
|
|
Vector3.Zero,
|
|
controller.StepUpHeight);
|
|
var snapped = new Vector3(
|
|
resolved.Position.X,
|
|
resolved.Position.Y,
|
|
resolved.Position.Z);
|
|
|
|
uint playerGuid = _identity.ServerGuid;
|
|
controller.SetPosition(
|
|
snapped,
|
|
resolved.CellId,
|
|
CellLocalForSeed(snapped, resolved.CellId));
|
|
|
|
// SnapToCell owns the retail Position frame and may normalize an
|
|
// outdoor land-cell index from the cell-local origin. Publish that
|
|
// canonical result, not the pre-snap resolver hint, to rendering.
|
|
if (_liveEntities.MaterializedWorldEntities.TryGetValue(
|
|
playerGuid,
|
|
out WorldEntity? entity))
|
|
{
|
|
entity.SetPosition(controller.Position);
|
|
entity.ParentCellId = controller.CellId;
|
|
entity.Rotation = rotation;
|
|
}
|
|
|
|
// Retail teleport_hook tail @ 0x00514ED0 clears the local target and
|
|
// notifies every watcher that this object teleported.
|
|
_host.Host?.NotifyTeleported();
|
|
controller.SetBodyOrientation(rotation);
|
|
|
|
_cameras.Legacy?.Update(controller.Position, controller.Yaw);
|
|
_cameras.Retail?.ResetViewerToPlayer(controller.Position, controller.Yaw);
|
|
_spatial.Reconcile();
|
|
|
|
PhysicsDiagnostics.LogTeleport(
|
|
"PLACED",
|
|
controller.CellId,
|
|
"readiness=complete");
|
|
Console.WriteLine(
|
|
$"live: teleport materialized - snapped to {controller.Position} "
|
|
+ $"cell=0x{controller.CellId:X8}");
|
|
}
|
|
|
|
private Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
|
|
{
|
|
int landblockX = (int)((cellId >> 24) & 0xFFu);
|
|
int landblockY = (int)((cellId >> 16) & 0xFFu);
|
|
var origin = new Vector3(
|
|
(landblockX - _origin.CenterX) * 192f,
|
|
(landblockY - _origin.CenterY) * 192f,
|
|
0f);
|
|
return worldPosition - origin;
|
|
}
|
|
}
|
|
|
|
internal interface ILocalPlayerTeleportSession
|
|
{
|
|
void SendLoginComplete();
|
|
}
|
|
|
|
internal sealed class LocalPlayerTeleportSession : ILocalPlayerTeleportSession
|
|
{
|
|
private readonly LiveSessionController _session;
|
|
|
|
public LocalPlayerTeleportSession(LiveSessionController session) =>
|
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
|
|
|
public void SendLoginComplete() =>
|
|
_session.CurrentSession?.SendGameAction(
|
|
GameActionLoginComplete.Build());
|
|
}
|
|
|
|
internal interface ILocalPlayerTeleportPresentation : IDisposable
|
|
{
|
|
bool IsPortalViewportVisible { get; }
|
|
int CurrentTunnelFrame { get; }
|
|
void Begin(Matrix4x4 projection);
|
|
(TeleportAnimSnapshot Snapshot, IReadOnlyList<TeleportAnimEvent> Events)
|
|
Tick(float deltaSeconds, bool worldReady);
|
|
void TickTunnel(float deltaSeconds);
|
|
void EnterTunnel();
|
|
void ExitTunnel();
|
|
void SetWaitCue(bool visible);
|
|
void Reset();
|
|
Matrix4x4 ApplyViewPlane(Matrix4x4 projection);
|
|
ICamera ApplyViewPlane(ICamera camera);
|
|
void DrawPortalViewport(int width, int height, Matrix4x4 projection);
|
|
}
|
|
|
|
internal sealed class LocalPlayerTeleportPresentation
|
|
: ILocalPlayerTeleportPresentation
|
|
{
|
|
private readonly TeleportAnimSequencer _animation = new();
|
|
private readonly TeleportViewPlaneController _viewPlane = new();
|
|
private readonly PortalTunnelPresentation _tunnel;
|
|
|
|
public LocalPlayerTeleportPresentation(PortalTunnelPresentation tunnel) =>
|
|
_tunnel = tunnel ?? throw new ArgumentNullException(nameof(tunnel));
|
|
|
|
public bool IsPortalViewportVisible => _tunnel.IsVisible;
|
|
public int CurrentTunnelFrame => _tunnel.CurrentAnimationFrame;
|
|
|
|
public void Begin(Matrix4x4 projection)
|
|
{
|
|
_viewPlane.Begin(projection);
|
|
_animation.Begin(TeleportEntryKind.Portal);
|
|
}
|
|
|
|
public (TeleportAnimSnapshot Snapshot, IReadOnlyList<TeleportAnimEvent> Events)
|
|
Tick(float deltaSeconds, bool worldReady)
|
|
{
|
|
var (snapshot, events) = _animation.Tick(
|
|
deltaSeconds,
|
|
worldReady,
|
|
CurrentTunnelFrame);
|
|
_viewPlane.Update(snapshot);
|
|
return (snapshot, events);
|
|
}
|
|
|
|
public void TickTunnel(float deltaSeconds) => _tunnel.Tick(deltaSeconds);
|
|
public void EnterTunnel() => _tunnel.Enter();
|
|
public void ExitTunnel() => _tunnel.Exit();
|
|
public void SetWaitCue(bool visible) => _tunnel.SetWaitCue(visible);
|
|
|
|
public void Reset()
|
|
{
|
|
_animation.Reset();
|
|
_viewPlane.Reset();
|
|
_tunnel.Exit();
|
|
}
|
|
|
|
public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) =>
|
|
_viewPlane.Apply(projection);
|
|
|
|
public ICamera ApplyViewPlane(ICamera camera) => _viewPlane.ApplyTo(camera);
|
|
|
|
public void DrawPortalViewport(int width, int height, Matrix4x4 projection) =>
|
|
_tunnel.Draw(width, height, ApplyViewPlane(projection));
|
|
|
|
public void Dispose() => _tunnel.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single update-thread owner of the local F751/Position-correlated portal
|
|
/// lifetime: activation, destination aim, readiness hold, Place, viewport
|
|
/// transitions, LoginComplete, and session/reset symmetry.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerTeleportController
|
|
: ILocalPlayerTeleportFramePhase,
|
|
ILocalPlayerTeleportNetworkSink,
|
|
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
|
IDisposable
|
|
{
|
|
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
|
|
private readonly ILocalPlayerTeleportAuthority _authority;
|
|
private readonly ILocalPlayerTeleportInputLifetime _input;
|
|
private readonly ILocalPlayerTeleportModeOperations _mode;
|
|
private readonly ILocalPlayerTeleportStreamingOperations _streaming;
|
|
private readonly WorldRevealCoordinator _worldReveal;
|
|
private readonly ILocalPlayerTeleportPlacement _placement;
|
|
private readonly ILocalPlayerTeleportSession _session;
|
|
private readonly ILocalPlayerTeleportPresentation _presentation;
|
|
|
|
private Vector3 _pendingPosition;
|
|
private uint _pendingCell;
|
|
private Quaternion _pendingRotation = Quaternion.Identity;
|
|
private bool _hasAcceptedDestination;
|
|
private WorldSession.EntityPositionUpdate _acceptedDestination;
|
|
private float _holdSeconds;
|
|
private long _lifetimeGeneration;
|
|
private bool _disposed;
|
|
|
|
public LocalPlayerTeleportController(
|
|
ILocalPlayerTeleportAuthority authority,
|
|
ILocalPlayerTeleportInputLifetime input,
|
|
ILocalPlayerTeleportModeOperations mode,
|
|
ILocalPlayerTeleportStreamingOperations streaming,
|
|
WorldRevealCoordinator worldReveal,
|
|
ILocalPlayerTeleportPlacement placement,
|
|
ILocalPlayerTeleportSession session,
|
|
ILocalPlayerTeleportPresentation presentation)
|
|
{
|
|
_authority = authority ?? throw new ArgumentNullException(nameof(authority));
|
|
_input = input ?? throw new ArgumentNullException(nameof(input));
|
|
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
|
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
|
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
|
_placement = placement ?? throw new ArgumentNullException(nameof(placement));
|
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
|
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
|
}
|
|
|
|
public bool IsActive => _transit.IsActive;
|
|
public bool IsPortalViewportVisible => _presentation.IsPortalViewportVisible;
|
|
public uint ActiveDestinationCell => _transit.IsActive ? _pendingCell : 0u;
|
|
|
|
public void OnTeleportStarted(uint sequence)
|
|
{
|
|
ThrowIfDisposed();
|
|
ushort teleportSequence = (ushort)sequence;
|
|
if (!_authority.IsFreshStart(teleportSequence)
|
|
|| !_transit.CanBegin(teleportSequence))
|
|
{
|
|
return;
|
|
}
|
|
|
|
long observedGeneration = _lifetimeGeneration;
|
|
_input.EndMouseLook();
|
|
if (_lifetimeGeneration != observedGeneration)
|
|
return;
|
|
|
|
long resetGeneration = ResetTransit(clearSession: false);
|
|
if (_lifetimeGeneration != resetGeneration)
|
|
return;
|
|
|
|
_transit.QueueStart(teleportSequence);
|
|
TryActivatePendingPresentation();
|
|
Console.WriteLine($"live: teleport queued (seq={sequence})");
|
|
}
|
|
|
|
public void OfferDestination(
|
|
WorldSession.EntityPositionUpdate update,
|
|
bool teleportTimestampAdvanced)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (_transit.OfferDestination(
|
|
update.TeleportSequence,
|
|
teleportTimestampAdvanced,
|
|
update,
|
|
out WorldSession.EntityPositionUpdate destination))
|
|
{
|
|
AcceptDestination(destination);
|
|
}
|
|
}
|
|
|
|
public void Tick(float deltaSeconds)
|
|
{
|
|
ThrowIfDisposed();
|
|
TryActivatePendingPresentation();
|
|
TryAimAcceptedDestination();
|
|
if (!_transit.IsActive)
|
|
return;
|
|
|
|
long generation = _lifetimeGeneration;
|
|
ushort sequence = _transit.Sequence;
|
|
if (!_mode.TryEnterPortalSpace()
|
|
|| !IsCurrentLifetime(generation, sequence)
|
|
|| _mode.Controller is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool haveDestination = _pendingCell != 0u;
|
|
bool originReady = !_streaming.IsRecenterPending;
|
|
bool ready = haveDestination
|
|
&& originReady
|
|
&& _worldReveal.Evaluate(_pendingCell).IsReady;
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
|
|
if (haveDestination && !ready)
|
|
_holdSeconds += deltaSeconds;
|
|
_presentation.SetWaitCue(
|
|
haveDestination
|
|
&& !ready
|
|
&& _worldReveal.ObserveWait(
|
|
TimeSpan.FromSeconds(_holdSeconds)));
|
|
|
|
var (_, events) = _presentation.Tick(deltaSeconds, ready);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
|
|
foreach (TeleportAnimEvent teleportEvent in events)
|
|
{
|
|
switch (teleportEvent)
|
|
{
|
|
case TeleportAnimEvent.Place:
|
|
_placement.Place(
|
|
_pendingPosition,
|
|
_pendingCell,
|
|
_pendingRotation);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
_worldReveal.ObserveMaterialized();
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
break;
|
|
case TeleportAnimEvent.EnterTunnel:
|
|
_presentation.EnterTunnel();
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
break;
|
|
case TeleportAnimEvent.PlayExitSound:
|
|
_presentation.ExitTunnel();
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
break;
|
|
case TeleportAnimEvent.FireLoginComplete:
|
|
_mode.EnterWorld();
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
_session.SendLoginComplete();
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
_worldReveal.Complete();
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
ResetTransit(clearSession: false);
|
|
return;
|
|
default:
|
|
// ClientUISystem sound-table enum playback remains outside
|
|
// this already-accepted presentation extraction.
|
|
break;
|
|
}
|
|
}
|
|
|
|
_presentation.TickTunnel(deltaSeconds);
|
|
}
|
|
|
|
public void ResetSession()
|
|
{
|
|
ThrowIfDisposed();
|
|
ResetTransit(clearSession: true);
|
|
}
|
|
|
|
public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) =>
|
|
_presentation.ApplyViewPlane(projection);
|
|
|
|
public ICamera ApplyViewPlane(ICamera camera) =>
|
|
_presentation.ApplyViewPlane(camera);
|
|
|
|
public void DrawPortalViewport(int width, int height, Matrix4x4 projection) =>
|
|
_presentation.DrawPortalViewport(width, height, projection);
|
|
|
|
private void TryActivatePendingPresentation()
|
|
{
|
|
if (!_transit.HasPendingStart)
|
|
return;
|
|
|
|
long generation = _lifetimeGeneration;
|
|
if (!_mode.TryEnterPortalSpace()
|
|
|| _lifetimeGeneration != generation
|
|
|| !_transit.HasPendingStart)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_holdSeconds = 0f;
|
|
_presentation.Begin(_mode.Projection);
|
|
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
|
|
return;
|
|
|
|
bool hasBufferedDestination = _transit.Activate(out var destination);
|
|
if (hasBufferedDestination)
|
|
AcceptDestination(destination);
|
|
Console.WriteLine(
|
|
$"live: teleport presentation started (seq={_transit.Sequence})");
|
|
}
|
|
|
|
private void AcceptDestination(WorldSession.EntityPositionUpdate destination)
|
|
{
|
|
_acceptedDestination = destination;
|
|
_hasAcceptedDestination = true;
|
|
TryAimAcceptedDestination();
|
|
}
|
|
|
|
private void TryAimAcceptedDestination()
|
|
{
|
|
if (!_hasAcceptedDestination || !_transit.IsActive)
|
|
return;
|
|
|
|
long generation = _lifetimeGeneration;
|
|
ushort sequence = _transit.Sequence;
|
|
PlayerMovementController? controller = _mode.Controller;
|
|
if (controller is null)
|
|
{
|
|
if (!_mode.TryEnterPortalSpace()
|
|
|| !IsCurrentLifetime(generation, sequence))
|
|
{
|
|
return;
|
|
}
|
|
|
|
controller = _mode.Controller;
|
|
if (controller is null)
|
|
return;
|
|
}
|
|
|
|
WorldSession.EntityPositionUpdate destination = _acceptedDestination;
|
|
if (!AimDestination(destination, controller, generation, sequence))
|
|
return;
|
|
|
|
if (IsCurrentLifetime(generation, sequence))
|
|
{
|
|
_hasAcceptedDestination = false;
|
|
_acceptedDestination = default;
|
|
}
|
|
}
|
|
|
|
private bool AimDestination(
|
|
WorldSession.EntityPositionUpdate update,
|
|
PlayerMovementController controller,
|
|
long generation,
|
|
ushort sequence)
|
|
{
|
|
var position = update.Position;
|
|
int landblockX = (int)((position.LandblockId >> 24) & 0xFFu);
|
|
int landblockY = (int)((position.LandblockId >> 16) & 0xFFu);
|
|
uint streamingOriginLandblockId = StreamingRegion.EncodeLandblockId(
|
|
_streaming.CenterX,
|
|
_streaming.CenterY);
|
|
var origin = new Vector3(
|
|
(landblockX - _streaming.CenterX) * 192f,
|
|
(landblockY - _streaming.CenterY) * 192f,
|
|
0f);
|
|
var translated = new Vector3(
|
|
position.PositionX,
|
|
position.PositionY,
|
|
position.PositionZ) + origin;
|
|
|
|
TeleportLandblockTransition transition = TeleportLandblockTransition.Classify(
|
|
controller.CellId,
|
|
position.LandblockId,
|
|
streamingOriginLandblockId);
|
|
int oldX = (int)((transition.SourceLandblockId >> 24) & 0xFFu);
|
|
int oldY = (int)((transition.SourceLandblockId >> 16) & 0xFFu);
|
|
|
|
Console.WriteLine(
|
|
$"live: teleport arrival - old lb=({oldX},{oldY}) "
|
|
+ $"new lb=({landblockX},{landblockY}) "
|
|
+ $"dist={Vector3.Distance(translated, controller.Position):F1}");
|
|
|
|
// Retail SmartBox enters blocking_for_cells before old-world object,
|
|
// physics, landscape, and ambient owners may advance again. Begin the
|
|
// reveal generation before a recenter can start detaching that world.
|
|
_worldReveal.Begin(WorldRevealKind.Portal, position.LandblockId);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return false;
|
|
|
|
Vector3 worldPosition;
|
|
if (transition.ChangesStreamingCenter)
|
|
{
|
|
bool isSealedDungeon = _streaming.IsSealedDungeon(
|
|
position.LandblockId);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return false;
|
|
|
|
_streaming.BeginRecenter(
|
|
landblockX,
|
|
landblockY,
|
|
isSealedDungeon);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return false;
|
|
worldPosition = new Vector3(
|
|
position.PositionX,
|
|
position.PositionY,
|
|
position.PositionZ);
|
|
}
|
|
else
|
|
{
|
|
worldPosition = translated;
|
|
}
|
|
|
|
_pendingRotation = new Quaternion(
|
|
position.RotationX,
|
|
position.RotationY,
|
|
position.RotationZ,
|
|
position.RotationW);
|
|
_pendingPosition = worldPosition;
|
|
_pendingCell = position.LandblockId;
|
|
_holdSeconds = 0f;
|
|
PhysicsDiagnostics.LogTeleport(
|
|
"AIM",
|
|
position.LandblockId,
|
|
$"seq={update.TeleportSequence} lb={landblockX},{landblockY} "
|
|
+ $"indoor={((position.LandblockId & 0xFFFFu) >= 0x0100u)} "
|
|
+ $"playerCross={transition.CrossesLandblock} "
|
|
+ $"centerChange={transition.ChangesStreamingCenter}");
|
|
return true;
|
|
}
|
|
|
|
private long ResetTransit(bool clearSession)
|
|
{
|
|
long generation = checked(++_lifetimeGeneration);
|
|
if (clearSession)
|
|
{
|
|
_transit.ClearSession();
|
|
}
|
|
else
|
|
{
|
|
_transit.EndActive();
|
|
}
|
|
|
|
_pendingPosition = default;
|
|
_pendingCell = 0u;
|
|
_pendingRotation = Quaternion.Identity;
|
|
_hasAcceptedDestination = false;
|
|
_acceptedDestination = default;
|
|
_holdSeconds = 0f;
|
|
|
|
_streaming.ResetRecenter(clearSession);
|
|
if (_lifetimeGeneration != generation)
|
|
return generation;
|
|
|
|
_worldReveal.Cancel();
|
|
if (_lifetimeGeneration != generation)
|
|
return generation;
|
|
|
|
_presentation.Reset();
|
|
if (_lifetimeGeneration != generation)
|
|
return generation;
|
|
|
|
return generation;
|
|
}
|
|
|
|
private bool IsCurrentLifetime(long generation, ushort sequence) =>
|
|
_lifetimeGeneration == generation
|
|
&& _transit.IsActive
|
|
&& _transit.Sequence == sequence;
|
|
|
|
private void ThrowIfDisposed() =>
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
_presentation.Dispose();
|
|
_disposed = true;
|
|
}
|
|
}
|