Slice A4 hung UI_EnterPortal on TeleportAnimEvent.EnterTunnel — the first tunnel-family frame — so the cue landed a whole TunnelFadeIn after retail plays it. Retail's site is gmSmartBoxUI::BeginTeleportAnimation @0x004D638E, i.e. Begin(), which the sequencer already marks as TeleportAnimEvent.PlayEnterSound. That event has existed since the R6 portal-space work, complete with a 'Begin(): sound_ui_enter_portal' comment, and no consumer has ever handled it — the switch in LocalPlayerTeleportController had cases for Place, EnterTunnel, PlayExitSound and FireLoginComplete only, so the sequencer emitted PlayEnterSound into nothing. A4 filled the gap in the wrong place rather than filling it. Both cues now go through named presentation methods driven by the matching events: PlayEnterCue on PlayEnterSound, PlayExitCue on PlayExitSound (the TunnelFadeOut -> WorldFadeIn edge, @0x004D7405, the same tick the world viewport is revealed). EnterTunnel/ExitTunnel are visuals again. The exit cue was already firing at the right moment, since ExitTunnel was called from inside the PlayExitSound case — correct by accident, explicit now. Found by the user asking when the recall cues play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1075 lines
44 KiB
C#
1075 lines
44 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.Audio;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.World;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Physics;
|
|
using AcDream.Runtime.Session;
|
|
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(Quaternion rotation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 3: acknowledges the local player's deferred portal arrival —
|
|
/// the canonical placement itself now runs through
|
|
/// <see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival"/>
|
|
/// (a portal arm sharing route 2's Runtime SetPosition owner), retiring the
|
|
/// duplicate Resolve/SetPosition authority this class used to own (D1;
|
|
/// docs/research/2026-08-04-c4-route-3-contract.md D-T4). This class runs
|
|
/// AFTER that commit succeeds. A10 review fix (2026-08-05): the render
|
|
/// <see cref="WorldEntity"/> pose write and rebucket this method performs
|
|
/// are REDUNDANT repeats of a mutation the canonical Place receipt already
|
|
/// made — <c>RuntimePlacementPresentationSink.TryApply</c> →
|
|
/// <c>LiveEntityRuntime.TryApplyRuntimePlacementPlace</c> already calls
|
|
/// <c>entity.SetPosition</c>/sets <c>Rotation</c>/<c>ParentCellId</c> and
|
|
/// rebuckets, synchronously, before <c>TryPublishPlace</c>'s OWN snapshot
|
|
/// even runs (proof obligation P2's ordering). This method's writes are
|
|
/// therefore harmless-but-duplicate, not the render entity's ONLY mover as
|
|
/// an earlier revision of this comment claimed; kept because they cost
|
|
/// nothing extra and this is also where the retail teleport_hook tail's
|
|
/// remaining local-player-visible actions run (target-watcher
|
|
/// notification, camera reset, spatial reconcile).
|
|
/// <paramref name="rotation"/> is the retained accepted destination's wire
|
|
/// orientation — the resolved body orientation was already committed
|
|
/// identically by <c>CommitCanonical</c> (retail's teleport branch does not
|
|
/// independently reorient the mover), so re-deriving it here would only add
|
|
/// a second copy of the same source of truth.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlacement
|
|
{
|
|
private readonly LiveEntityRuntime _liveEntities;
|
|
private readonly ILocalPlayerIdentitySource _identity;
|
|
private readonly IRuntimeLocalPlayerControllerSource _controller;
|
|
private readonly ILocalPlayerPhysicsHostSource _host;
|
|
private readonly ChaseCameraInputState _cameras;
|
|
private readonly ILiveSpatialReconcilePhase _spatial;
|
|
|
|
public LocalPlayerTeleportPlacement(
|
|
LiveEntityRuntime liveEntities,
|
|
ILocalPlayerIdentitySource identity,
|
|
IRuntimeLocalPlayerControllerSource controller,
|
|
ILocalPlayerPhysicsHostSource host,
|
|
ChaseCameraInputState cameras,
|
|
ILiveSpatialReconcilePhase spatial)
|
|
{
|
|
_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));
|
|
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
|
}
|
|
|
|
public void Place(Quaternion rotation)
|
|
{
|
|
PlayerMovementController controller = _controller.Controller
|
|
?? throw new InvalidOperationException(
|
|
"Teleport Place ran without the local player controller.");
|
|
|
|
uint playerGuid = _identity.ServerGuid;
|
|
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 canonical commit has already advanced FullCellId, but that
|
|
// 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. The body's
|
|
// constraint leash re-arm and orientation are already the canonical
|
|
// commit's job (RuntimeAcceptedPositionDriveController
|
|
// .ReconcileAndAcknowledgePortal -> PlayerMovementController
|
|
// .CommitCanonicalTeleportFrame), so this suffix only acknowledges
|
|
// the result into presentation.
|
|
_host.Host?.NotifyTeleported();
|
|
|
|
_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}");
|
|
}
|
|
}
|
|
|
|
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 PlayEnterCue();
|
|
void PlayExitCue();
|
|
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);
|
|
|
|
/// <summary>
|
|
/// Interface-sound sink for the portal cues. Retail plays these from the
|
|
/// teleport-animation boundary, not from the tunnel renderer:
|
|
/// <c>PlaySoundFromCenter(Sound_UI_EnterPortal, GetUISoundTable())</c> at
|
|
/// <c>0x004D638E</c> inside <c>gmSmartBoxUI::BeginTeleportAnimation</c>, and
|
|
/// <c>Sound_UI_ExitPortal</c> at <c>0x004D7405</c>. Assigned at composition;
|
|
/// null (and therefore silent) when audio is unavailable.
|
|
/// </summary>
|
|
public Action<SoundId>? UiSoundSink { get; set; }
|
|
|
|
/// <summary>
|
|
/// <c>Sound_UI_EnterPortal</c>, at retail's moment: the START of the
|
|
/// teleport animation (<c>gmSmartBoxUI::BeginTeleportAnimation</c> @
|
|
/// <c>0x004D638E</c>), which the sequencer marks as
|
|
/// <c>TeleportAnimEvent.PlayEnterSound</c> — NOT when the tunnel viewport
|
|
/// first appears, which is a TunnelFadeIn later.
|
|
/// </summary>
|
|
public void PlayEnterCue() => UiSoundSink?.Invoke(SoundId.UI_EnterPortal);
|
|
|
|
/// <summary>
|
|
/// <c>Sound_UI_ExitPortal</c> @ <c>0x004D7405</c>, at the
|
|
/// TunnelFadeOut to WorldFadeIn edge — the sequencer's
|
|
/// <c>TeleportAnimEvent.PlayExitSound</c>, the same tick the world viewport
|
|
/// is revealed.
|
|
/// </summary>
|
|
public void PlayExitCue() => UiSoundSink?.Invoke(SoundId.UI_ExitPortal);
|
|
|
|
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 readonly RuntimeAcceptedPositionDriveController _acceptedPositionDrive;
|
|
|
|
private uint _pendingCell;
|
|
private Quaternion _pendingRotation = Quaternion.Identity;
|
|
private long _pendingRevealGeneration;
|
|
private RuntimeTeleportDestination _pendingDestination;
|
|
private bool _hasPendingDestination;
|
|
|
|
/// <summary>
|
|
/// A1 review fix (2026-08-05): true once the canonical Runtime commit
|
|
/// for THIS teleport lifetime has actually happened. See the class doc
|
|
/// on <see cref="TryAdvancePortalCommit"/> for why this exists.
|
|
/// </summary>
|
|
private bool _placementCommitted;
|
|
|
|
/// <summary>
|
|
/// A1 review fix: true while a <c>DeferredCell</c> park is outstanding
|
|
/// for the local player's one possible pending operation. See
|
|
/// <see cref="TryAdvancePortalCommit"/>.
|
|
/// </summary>
|
|
private bool _awaitingDeferredWake;
|
|
|
|
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,
|
|
RuntimeAcceptedPositionDriveController acceptedPositionDrive)
|
|
{
|
|
_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));
|
|
_acceptedPositionDrive = acceptedPositionDrive
|
|
?? throw new ArgumentNullException(nameof(acceptedPositionDrive));
|
|
}
|
|
|
|
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 dataReady = haveDestination
|
|
&& originReady
|
|
&& _worldReveal.Evaluate(_pendingCell).IsReady;
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
|
|
// A1 review fix (2026-08-05, retail/architecture review): the
|
|
// sequencer's Tunnel -> TunnelContinue transition
|
|
// (TeleportAnimSequencer.cs:134-141) is unconditional and
|
|
// irreversible the instant it observes `worldReady` true; by the
|
|
// time a failed placement is discovered the stream has already left
|
|
// Tunnel with no path back, and TeleportAnimSequencer itself is
|
|
// untouched (stop condition 2 forbids sequencer timing changes). So
|
|
// the boolean fed into the sequencer must never mean "the data is
|
|
// ready" alone - it must mean "the canonical Runtime commit has
|
|
// ALREADY happened", checked/attempted fresh every tick via
|
|
// TryAdvancePortalCommit. This makes D-T5 row 2's "the NEXT Tick
|
|
// re-attempts the Place edge" real: the sequencer simply never
|
|
// leaves Tunnel while the commit keeps refusing, and by the time it
|
|
// finally does leave Tunnel and fire Place, TryAdvancePortalCommit
|
|
// has already made the Runtime side succeed - the Place-event
|
|
// handler below only ever runs the presentation suffix.
|
|
bool placementReady = dataReady && TryAdvancePortalCommit(sequence);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
|
|
if (haveDestination && !placementReady)
|
|
_holdSeconds += deltaSeconds;
|
|
_presentation.SetWaitCue(
|
|
haveDestination
|
|
&& !placementReady
|
|
&& _worldReveal.ObserveWait(
|
|
TimeSpan.FromSeconds(_holdSeconds)));
|
|
|
|
var (_, events) = _presentation.Tick(deltaSeconds, placementReady);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
|
|
foreach (TeleportAnimEvent teleportEvent in events)
|
|
{
|
|
switch (teleportEvent)
|
|
{
|
|
case TeleportAnimEvent.Place:
|
|
// TryAdvancePortalCommit above is the only path that
|
|
// makes `placementReady` (and, with the REAL sequencer,
|
|
// this event) true, so the canonical Runtime commit has
|
|
// ALREADY succeeded by construction on that path - this
|
|
// only runs the presentation suffix (D-T4). The
|
|
// _placementCommitted re-check stays defensive: it is
|
|
// the exact same shape of guard IsCurrentLifetime below
|
|
// already applies to every other step of this case, for
|
|
// a transit that goes stale between the gate above and
|
|
// this line being reached.
|
|
if (!_placementCommitted)
|
|
return;
|
|
// B7 review fix (2026-08-05): re-derived, not assumed -
|
|
// if the reveal was cancelled/superseded in the window
|
|
// between the commit above and this event being
|
|
// processed, ObserveMaterialized below would refuse but
|
|
// Place/the presentation suffix would already have run
|
|
// against a reveal that is no longer current. Same
|
|
// check TryExecuteCanonicalPortalPlacementCore itself
|
|
// gates on; idempotent to repeat here.
|
|
if (!_worldReveal.CanPlacePortalDestination(
|
|
_pendingRevealGeneration, sequence, _pendingCell))
|
|
{
|
|
return;
|
|
}
|
|
_placement.Place(_pendingRotation);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
_worldReveal.ObserveMaterialized(
|
|
_pendingRevealGeneration,
|
|
sequence,
|
|
_pendingCell);
|
|
if (!IsCurrentLifetime(generation, sequence))
|
|
return;
|
|
break;
|
|
case TeleportAnimEvent.PlayEnterSound:
|
|
// Retail plays the enter cue as the animation BEGINS, before
|
|
// the tunnel is visible. The sequencer has always emitted
|
|
// this event; nothing consumed it until Campaign A.
|
|
_presentation.PlayEnterCue();
|
|
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.PlayExitCue();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A1 review fix (2026-08-05): the one gate that decides whether the
|
|
/// anim sequencer is allowed to see <c>worldReady=true</c>. Returns
|
|
/// <see langword="true"/> ONLY once the canonical Runtime commit for
|
|
/// THIS teleport lifetime has actually happened — never speculatively,
|
|
/// never optimistically. Three states:
|
|
///
|
|
/// <list type="bullet">
|
|
/// <item><description>Already committed
|
|
/// (<see cref="_placementCommitted"/>) — returns
|
|
/// <see langword="true"/> immediately, every subsequent Tick.</description></item>
|
|
/// <item><description>A <c>DeferredCell</c> park is outstanding
|
|
/// (<see cref="_awaitingDeferredWake"/>) — polls
|
|
/// <see cref="RuntimeAcceptedPositionDriveController.PendingCount"/>
|
|
/// only to decide whether to re-attempt the Runtime call: Runtime's own
|
|
/// Begin would just refuse a second overlapping attempt with
|
|
/// <c>Contention</c> while a park is outstanding (the drive tracks at
|
|
/// most one pending operation for the local player), so retrying blind
|
|
/// would only add noise. B1 review fix (2026-08-05): once
|
|
/// <c>PendingCount</c> returns to 0 the park is DONE, but "done" is not
|
|
/// "committed" — the drive's own doc names a merge-time <c>Forget</c>
|
|
/// (an ordinary ACE broadcast arriving mid-park) as the EXPECTED way a
|
|
/// park resolves without committing, and A2's own abandon branches are
|
|
/// a second way. The OLD code inferred commit from the empty slot
|
|
/// alone; this now asks
|
|
/// <see cref="RuntimeAcceptedPositionDriveController.TryConsumePortalCommit"/>,
|
|
/// which the drive latches ONLY inside a REAL
|
|
/// <c>ReconcileAndAcknowledgePortal</c> call, keyed to this exact
|
|
/// reveal generation/sequence. A "no" here is NOT a failure — it just
|
|
/// means the park ended without placing, so <see cref="_awaitingDeferredWake"/>
|
|
/// clears and the method falls through to a fresh attempt below,
|
|
/// safely (nothing is pending anymore).</description></item>
|
|
/// <item><description>Neither — attempt the canonical placement fresh
|
|
/// this tick. <c>Committed</c> latches
|
|
/// <see cref="_placementCommitted"/>; <c>DeferredCell</c> latches
|
|
/// <see cref="_awaitingDeferredWake"/>; every other status (Contention,
|
|
/// Rejected, NotApplicable, or the transit no longer owning this
|
|
/// reveal) is the D-T5 refusal shape — nothing mutates, and the SAME
|
|
/// predicate retries automatically on the NEXT Tick, which is what
|
|
/// makes D-T5 row 2's "the next Tick re-attempts the Place edge"
|
|
/// mechanism real without ever touching
|
|
/// <see cref="AcDream.Core.World.TeleportAnimSequencer"/>.</description></item>
|
|
/// </list>
|
|
/// </summary>
|
|
private bool TryAdvancePortalCommit(ushort sequence)
|
|
{
|
|
if (_placementCommitted)
|
|
return true;
|
|
|
|
if (_awaitingDeferredWake)
|
|
{
|
|
if (_acceptedPositionDrive.PendingCount != 0)
|
|
return false;
|
|
_awaitingDeferredWake = false;
|
|
if (_acceptedPositionDrive.TryConsumePortalCommit(
|
|
_pendingRevealGeneration, sequence))
|
|
{
|
|
_placementCommitted = true;
|
|
return true;
|
|
}
|
|
// The park ended without placing (Forgotten, or abandoned by
|
|
// A2's re-validation). Fall through to the fresh-attempt path
|
|
// below in this SAME call — nothing is pending, so it is safe.
|
|
}
|
|
|
|
if (!_worldReveal.CanPlacePortalDestination(
|
|
_pendingRevealGeneration,
|
|
sequence,
|
|
_pendingCell))
|
|
{
|
|
PhysicsDiagnostics.LogTeleport(
|
|
"REFUSED", _pendingCell, "cause=stale-reveal");
|
|
// R8 residual fix (2026-08-05): this refusal previously only
|
|
// logged through PhysicsDiagnostics.LogTeleport, gated by the
|
|
// DIFFERENT ACDREAM_PROBE_TELEPORT flag — invisible under
|
|
// ACDREAM_PROBE_LOCAL_TELEPORT, the gate the rest of this
|
|
// route's arrival/commit lines use. No placement was attempted,
|
|
// so there is no resolved cell/leash/autorun fact to report.
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "stale-reveal",
|
|
placementStatus: "Refused",
|
|
portalGeneration: _pendingRevealGeneration,
|
|
teleportSequence: sequence,
|
|
destinationCell: _pendingCell,
|
|
resolvedCell: 0u,
|
|
hookTailRan: false,
|
|
leashArmed: false,
|
|
autorunCancelled: false);
|
|
return false;
|
|
}
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
TryExecuteCanonicalPortalPlacementCore(sequence);
|
|
switch (status)
|
|
{
|
|
case RuntimeAcceptedPositionExecutionStatus.Committed:
|
|
_placementCommitted = true;
|
|
return true;
|
|
case RuntimeAcceptedPositionExecutionStatus.DeferredCell:
|
|
_awaitingDeferredWake = true;
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 3 (D-T1/D-T2): builds the producer's
|
|
/// <see cref="RuntimePortalPlacementAuthority"/> from live transit facts
|
|
/// and drives the canonical Runtime portal arm. No new
|
|
/// <see cref="WorldRevealCoordinator"/> exposure is needed — the host
|
|
/// token is RE-DERIVED through the transit owner's idempotent
|
|
/// <c>TryRegisterHostProjection</c> (the same generation+cell returns
|
|
/// the token <see cref="WorldRevealCoordinator.TryBeginPortal"/> already
|
|
/// registered at Aim time; a stale generation, wrong cell, cancelled, or
|
|
/// completed reveal refuses), which makes a superseded token unobtainable
|
|
/// by construction.
|
|
///
|
|
/// <para>
|
|
/// The destination itself is <see cref="_pendingDestination"/> — the
|
|
/// value <see cref="AimDestination"/> captured at Aim time — and NOT a
|
|
/// fresh <c>_transit.TryGetAcceptedTeleportDestination</c> read.
|
|
/// <see cref="RuntimeWorldTransitState.TryBeginPortalReveal"/> atomically
|
|
/// CONSUMES the transit's one accepted-destination slot the instant Aim
|
|
/// claims the reveal generation (it clears
|
|
/// <c>_hasAcceptedDestination</c> so a stale destination can never be
|
|
/// re-claimed by a later portal) — by Place time that slot is already
|
|
/// empty, so re-querying it here always fails. This mirrors why
|
|
/// <see cref="_pendingCell"/>/<see cref="_pendingRotation"/>/
|
|
/// <see cref="_pendingRevealGeneration"/> are themselves Aim-time
|
|
/// snapshots rather than live transit reads.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// A9 review fix (2026-08-05): <paramref name="sequence"/> is
|
|
/// <see cref="_pendingDestination"/>'s OWN
|
|
/// <see cref="RuntimeTeleportDestination.TeleportSequence"/>, not the
|
|
/// transit's separately-tracked <c>ActiveTeleportSequence</c> the caller
|
|
/// otherwise threads through — one source for the fact this method's
|
|
/// authority carries, asserted equal to the caller's copy so the two
|
|
/// can never silently diverge.
|
|
/// </para>
|
|
/// </summary>
|
|
private RuntimeAcceptedPositionExecutionStatus
|
|
TryExecuteCanonicalPortalPlacementCore(ushort sequence)
|
|
{
|
|
System.Diagnostics.Debug.Assert(
|
|
!_hasPendingDestination
|
|
|| _pendingDestination.TeleportSequence == sequence,
|
|
"The transit's active sequence and the Aim-time destination's "
|
|
+ "own sequence must never diverge (A9).");
|
|
if (!_hasPendingDestination
|
|
|| !_transit.TryRegisterHostProjection(
|
|
_pendingRevealGeneration,
|
|
_pendingCell,
|
|
out RuntimeWorldHostProjectionToken hostToken))
|
|
{
|
|
PhysicsDiagnostics.LogTeleport(
|
|
"REFUSED", _pendingCell, "cause=host-token-unavailable");
|
|
// R8 residual fix (2026-08-05): same rationale as the
|
|
// stale-reveal refusal above — route through
|
|
// LogLocalTeleportArrival too, so ACDREAM_PROBE_LOCAL_TELEPORT
|
|
// alone is enough to see every App-side refusal cause.
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "host-token-unavailable",
|
|
placementStatus: "Refused",
|
|
portalGeneration: _pendingRevealGeneration,
|
|
teleportSequence: sequence,
|
|
destinationCell: _pendingCell,
|
|
resolvedCell: 0u,
|
|
hookTailRan: false,
|
|
leashArmed: false,
|
|
autorunCancelled: false);
|
|
return RuntimeAcceptedPositionExecutionStatus.Rejected;
|
|
}
|
|
|
|
RuntimeTeleportDestination destination = _pendingDestination;
|
|
var portal = new RuntimePortalPlacementAuthority(
|
|
Present: true,
|
|
RevealGeneration: _pendingRevealGeneration,
|
|
TeleportSequence: destination.TeleportSequence,
|
|
Projection: hostToken);
|
|
return _acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
|
|
destination,
|
|
portal);
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
// C4 route 3: the App-frame-translated `translated`/`worldPosition`
|
|
// vector is no longer carried past this point — the canonical
|
|
// portal arm resolves the placement through Runtime's OWN world
|
|
// frame (resolveWorldOffsetFromRuntimeFrame: true), using the
|
|
// cell-local `destination` Position captured HERE rather than an
|
|
// App-translated snapshot (trap T4). It must be captured here and
|
|
// NOT re-read from the transit at the Place edge:
|
|
// _worldReveal.TryBeginPortal (above) drives
|
|
// RuntimeWorldTransitState.TryBeginPortalReveal, which atomically
|
|
// CONSUMES the transit's one accepted-destination slot the instant
|
|
// it claims this reveal generation — a later
|
|
// TryGetAcceptedTeleportDestination call always finds it empty.
|
|
_pendingRotation = position.Frame.Orientation;
|
|
_pendingCell = position.ObjCellId;
|
|
_pendingDestination = destination;
|
|
_hasPendingDestination = true;
|
|
_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);
|
|
|
|
_pendingCell = 0u;
|
|
_pendingRotation = Quaternion.Identity;
|
|
_pendingRevealGeneration = 0;
|
|
_pendingDestination = default;
|
|
_hasPendingDestination = false;
|
|
_placementCommitted = false;
|
|
_awaitingDeferredWake = false;
|
|
_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;
|
|
}
|
|
}
|