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(); } /// /// Construction-order bridge for the inbound session. It carries no teleport /// state: after binding, every packet and reset is forwarded to the one /// owner. /// 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); } /// /// Commits the local player's deferred portal arrival. It owns the exact /// Place -> root/controller/camera mutation -> spatial reconcile edge. /// 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 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 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(); } /// /// 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. /// internal sealed class LocalPlayerTeleportController : ILocalPlayerTeleportFramePhase, ILocalPlayerTeleportNetworkSink, AcDream.App.Interaction.ISelectionViewPlaneSource, IDisposable { private readonly TeleportTransitCoordinator _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; } }