Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
789 lines
28 KiB
C#
789 lines
28 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.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.World;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.World;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
internal interface ILocalPlayerTeleportNetworkSink
|
|
{
|
|
void OnTeleportStarted(uint sequence);
|
|
|
|
void OfferDestination(
|
|
RuntimeTeleportDestination destination,
|
|
bool teleportTimestampAdvanced);
|
|
|
|
void ResetSession();
|
|
|
|
void ResetGenerationPresentation();
|
|
}
|
|
|
|
/// <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(
|
|
RuntimeTeleportDestination destination,
|
|
bool teleportTimestampAdvanced) =>
|
|
Required().OfferDestination(destination, teleportTimestampAdvanced);
|
|
|
|
public void ResetSession() => Required().ResetSession();
|
|
|
|
public void ResetGenerationPresentation() =>
|
|
Required().ResetGenerationPresentation();
|
|
|
|
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 IRuntimeLocalPlayerControllerSource _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,
|
|
IRuntimeLocalPlayerControllerSource 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.TryGetWorldEntity(
|
|
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>
|
|
/// Graphical-host orchestrator for Runtime's local F751/Position-correlated
|
|
/// portal lifetime. Runtime owns sequence/destination/reveal authority; this
|
|
/// update-thread adapter owns portal viewport activation, render-space aim,
|
|
/// readiness preparation, placement callbacks, and LoginComplete presentation.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerTeleportController
|
|
: ILocalPlayerTeleportFramePhase,
|
|
ILocalPlayerTeleportNetworkSink,
|
|
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
|
IDisposable
|
|
{
|
|
private readonly RuntimeWorldTransitState _transit;
|
|
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 long _pendingRevealGeneration;
|
|
private float _holdSeconds;
|
|
private long _lifetimeGeneration;
|
|
private bool _disposed;
|
|
|
|
public LocalPlayerTeleportController(
|
|
ILocalPlayerTeleportAuthority authority,
|
|
ILocalPlayerTeleportInputLifetime input,
|
|
ILocalPlayerTeleportModeOperations mode,
|
|
ILocalPlayerTeleportStreamingOperations streaming,
|
|
RuntimeWorldTransitState transit,
|
|
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));
|
|
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
|
|
_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.IsTeleportActive;
|
|
public bool IsPortalViewportVisible => _presentation.IsPortalViewportVisible;
|
|
public uint ActiveDestinationCell =>
|
|
_transit.IsTeleportActive ? _pendingCell : 0u;
|
|
|
|
public void OnTeleportStarted(uint sequence)
|
|
{
|
|
ThrowIfDisposed();
|
|
ushort teleportSequence = (ushort)sequence;
|
|
if (!_authority.IsFreshStart(teleportSequence)
|
|
|| !_transit.CanQueueTeleportStart(teleportSequence))
|
|
{
|
|
return;
|
|
}
|
|
|
|
long observedGeneration = _lifetimeGeneration;
|
|
_input.EndMouseLook();
|
|
if (_lifetimeGeneration != observedGeneration)
|
|
return;
|
|
|
|
long resetGeneration = ResetTransit(clearSession: false);
|
|
if (_lifetimeGeneration != resetGeneration)
|
|
return;
|
|
|
|
if (!_transit.TryQueueTeleportStart(teleportSequence))
|
|
return;
|
|
TryActivatePendingPresentation();
|
|
Console.WriteLine($"live: teleport queued (seq={sequence})");
|
|
}
|
|
|
|
public void OfferDestination(
|
|
RuntimeTeleportDestination destination,
|
|
bool teleportTimestampAdvanced)
|
|
{
|
|
ThrowIfDisposed();
|
|
_transit.OfferTeleportDestination(
|
|
destination,
|
|
teleportTimestampAdvanced);
|
|
TryAimAcceptedDestination();
|
|
}
|
|
|
|
public void Tick(float deltaSeconds)
|
|
{
|
|
ThrowIfDisposed();
|
|
TryActivatePendingPresentation();
|
|
TryAimAcceptedDestination();
|
|
if (!_transit.IsTeleportActive)
|
|
return;
|
|
|
|
long generation = _lifetimeGeneration;
|
|
ushort sequence = _transit.ActiveTeleportSequence;
|
|
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:
|
|
if (!_worldReveal.CanPlacePortalDestination(
|
|
_pendingRevealGeneration,
|
|
sequence,
|
|
_pendingCell))
|
|
{
|
|
return;
|
|
}
|
|
_placement.Place(
|
|
_pendingPosition,
|
|
_pendingCell,
|
|
_pendingRotation);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
_worldReveal.ObserveMaterialized(
|
|
_pendingRevealGeneration,
|
|
sequence,
|
|
_pendingCell);
|
|
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,
|
|
resetCanonicalTransit: true);
|
|
}
|
|
|
|
public void ResetGenerationPresentation()
|
|
{
|
|
ThrowIfDisposed();
|
|
ResetTransit(
|
|
clearSession: true,
|
|
resetCanonicalTransit: false);
|
|
}
|
|
|
|
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.HasPendingTeleportStart)
|
|
return;
|
|
|
|
long generation = _lifetimeGeneration;
|
|
if (!_mode.TryEnterPortalSpace()
|
|
|| _lifetimeGeneration != generation
|
|
|| !_transit.HasPendingTeleportStart)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_holdSeconds = 0f;
|
|
_presentation.Begin(_mode.Projection);
|
|
if (_lifetimeGeneration != generation
|
|
|| !_transit.HasPendingTeleportStart)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_transit.ActivateQueuedTeleport())
|
|
return;
|
|
TryAimAcceptedDestination();
|
|
Console.WriteLine(
|
|
$"live: teleport presentation started "
|
|
+ $"(seq={_transit.ActiveTeleportSequence})");
|
|
}
|
|
|
|
private void TryAimAcceptedDestination()
|
|
{
|
|
if (!_transit.TryGetAcceptedTeleportDestination(
|
|
out RuntimeTeleportDestination destination))
|
|
{
|
|
return;
|
|
}
|
|
|
|
long generation = _lifetimeGeneration;
|
|
ushort sequence = _transit.ActiveTeleportSequence;
|
|
PlayerMovementController? controller = _mode.Controller;
|
|
if (controller is null)
|
|
{
|
|
if (!_mode.TryEnterPortalSpace()
|
|
|| !IsCurrentLifetime(generation, sequence))
|
|
{
|
|
return;
|
|
}
|
|
|
|
controller = _mode.Controller;
|
|
if (controller is null)
|
|
return;
|
|
}
|
|
|
|
if (!AimDestination(destination, controller, generation, sequence))
|
|
return;
|
|
}
|
|
|
|
private bool AimDestination(
|
|
RuntimeTeleportDestination destination,
|
|
PlayerMovementController controller,
|
|
long generation,
|
|
ushort sequence)
|
|
{
|
|
Position position = destination.Position;
|
|
int landblockX = (int)((position.ObjCellId >> 24) & 0xFFu);
|
|
int landblockY = (int)((position.ObjCellId >> 16) & 0xFFu);
|
|
uint streamingOriginLandblockId = StreamingRegion.EncodeLandblockId(
|
|
_streaming.CenterX,
|
|
_streaming.CenterY);
|
|
var origin = new Vector3(
|
|
(landblockX - _streaming.CenterX) * 192f,
|
|
(landblockY - _streaming.CenterY) * 192f,
|
|
0f);
|
|
Vector3 translated = position.Frame.Origin + origin;
|
|
|
|
TeleportLandblockTransition transition = TeleportLandblockTransition.Classify(
|
|
controller.CellId,
|
|
position.ObjCellId,
|
|
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.
|
|
if (!_worldReveal.TryBeginPortal(
|
|
sequence,
|
|
position.ObjCellId,
|
|
out long revealGeneration))
|
|
{
|
|
return false;
|
|
}
|
|
_pendingRevealGeneration = revealGeneration;
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return false;
|
|
|
|
Vector3 worldPosition;
|
|
if (transition.ChangesStreamingCenter)
|
|
{
|
|
bool isSealedDungeon = _streaming.IsSealedDungeon(
|
|
position.ObjCellId);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return false;
|
|
|
|
_streaming.BeginRecenter(
|
|
landblockX,
|
|
landblockY,
|
|
isSealedDungeon);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return false;
|
|
worldPosition = new Vector3(
|
|
position.Frame.Origin.X,
|
|
position.Frame.Origin.Y,
|
|
position.Frame.Origin.Z);
|
|
}
|
|
else
|
|
{
|
|
worldPosition = translated;
|
|
}
|
|
|
|
_pendingRotation = position.Frame.Orientation;
|
|
_pendingPosition = worldPosition;
|
|
_pendingCell = position.ObjCellId;
|
|
_holdSeconds = 0f;
|
|
PhysicsDiagnostics.LogTeleport(
|
|
"AIM",
|
|
position.ObjCellId,
|
|
$"seq={destination.TeleportSequence} lb={landblockX},{landblockY} "
|
|
+ $"indoor={((position.ObjCellId & 0xFFFFu) >= 0x0100u)} "
|
|
+ $"playerCross={transition.CrossesLandblock} "
|
|
+ $"centerChange={transition.ChangesStreamingCenter}");
|
|
return true;
|
|
}
|
|
|
|
private long ResetTransit(
|
|
bool clearSession,
|
|
bool resetCanonicalTransit = false)
|
|
{
|
|
long generation = checked(++_lifetimeGeneration);
|
|
|
|
_pendingPosition = default;
|
|
_pendingCell = 0u;
|
|
_pendingRotation = Quaternion.Identity;
|
|
_pendingRevealGeneration = 0;
|
|
_holdSeconds = 0f;
|
|
|
|
_streaming.ResetRecenter(clearSession);
|
|
if (_lifetimeGeneration != generation)
|
|
return generation;
|
|
|
|
if (clearSession)
|
|
{
|
|
if (resetCanonicalTransit)
|
|
_worldReveal.ResetSession();
|
|
else
|
|
_worldReveal.ResetHostSession();
|
|
}
|
|
else
|
|
{
|
|
_transit.EndTeleport();
|
|
_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.IsTeleportActive
|
|
&& _transit.ActiveTeleportSequence == sequence;
|
|
|
|
private void ThrowIfDisposed() =>
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
_presentation.Dispose();
|
|
_disposed = true;
|
|
}
|
|
}
|