Move the canonical WorldSession generation, connect/enter/tick/stop transaction, inbound subscription owner, and retryable teardown acknowledgements into AcDream.Runtime. Keep App as a borrowing graphical host with a single inertable command projection and no mirrored session state. Validated by 79 Runtime tests, 3,776 App tests with three existing skips, the Release solution build, and 8,428 complete Release tests with five existing skips. Co-authored-by: Codex <codex@openai.com>
766 lines
27 KiB
C#
766 lines
27 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 CPhysicsObj::enter_world installs the object in its
|
|
// destination CObjCell before hidden scripts/particles resume.
|
|
// The accepted Position packet has already advanced FullCellId,
|
|
// but that wire fact alone does not move acdream's retained
|
|
// projection out of its source/pending GPU bucket. Commit both
|
|
// halves of the spatial move here, while portal space still owns
|
|
// the viewport, so CPhysicsObj::update_object's cell-gated tail
|
|
// can advance the Hidden/UnHide PES chain at retail's boundary.
|
|
if (!_liveEntities.RebucketLiveEntity(playerGuid, controller.CellId))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Teleport Place could not commit local player 0x{playerGuid:X8} "
|
|
+ $"to destination cell 0x{controller.CellId:X8}.");
|
|
}
|
|
}
|
|
|
|
// 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 ILiveWorldSessionSource _session;
|
|
|
|
public LocalPlayerTeleportSession(ILiveWorldSessionSource 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:
|
|
// gmSmartBoxUI::UseTime @ 0x004D6E30 releases destination
|
|
// cell blocking at the exact portal/world viewport swap.
|
|
// LoginComplete remains one WorldFadeIn second later.
|
|
_worldReveal.RevealWorldViewport();
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
_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;
|
|
}
|
|
}
|