refactor(runtime): extract local teleport and player mode
Move local teleport, player-mode, animation, shadow, and sealed-dungeon lifetimes out of GameWindow. Make player-mode entry transactional and isolate approach completions by controller lifetime and approach generation so stale callbacks cannot affect replacements. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
c557038353
commit
eeb0f6b45c
29 changed files with 3311 additions and 1073 deletions
761
src/AcDream.App/Streaming/LocalPlayerTeleportController.cs
Normal file
761
src/AcDream.App/Streaming/LocalPlayerTeleportController.cs
Normal file
|
|
@ -0,0 +1,761 @@
|
|||
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 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.");
|
||||
}
|
||||
|
||||
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);
|
||||
void SetPriority(uint landblockId, int radius);
|
||||
void ClearPriority();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
public void SetPriority(uint landblockId, int radius)
|
||||
{
|
||||
_streaming.PriorityLandblockId = landblockId;
|
||||
_streaming.PriorityRadius = radius;
|
||||
}
|
||||
|
||||
public void ClearPriority()
|
||||
{
|
||||
_streaming.PriorityLandblockId = 0u;
|
||||
_streaming.PriorityRadius = 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerTeleportPlacement
|
||||
{
|
||||
void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced);
|
||||
}
|
||||
|
||||
/// <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, bool forced)
|
||||
{
|
||||
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);
|
||||
|
||||
if (forced)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: teleport HOLD gave up (impossible/timeout) - force-snapping "
|
||||
+ $"cell=0x{cellId:X8} pos={position} -> 0x{resolved.CellId:X8} {snapped}");
|
||||
}
|
||||
|
||||
uint playerGuid = _identity.ServerGuid;
|
||||
controller.SetPosition(
|
||||
snapped,
|
||||
resolved.CellId,
|
||||
CellLocalForSeed(snapped, resolved.CellId));
|
||||
|
||||
// SnapToCell owns the retail Position frame and may normalize an
|
||||
// outdoor land-cell index from the cell-local origin. Publish that
|
||||
// canonical result, not the pre-snap resolver hint, to rendering.
|
||||
if (_liveEntities.MaterializedWorldEntities.TryGetValue(
|
||||
playerGuid,
|
||||
out WorldEntity? entity))
|
||||
{
|
||||
entity.SetPosition(controller.Position);
|
||||
entity.ParentCellId = controller.CellId;
|
||||
entity.Rotation = rotation;
|
||||
}
|
||||
|
||||
// Retail teleport_hook tail @ 0x00514ED0 clears the local target and
|
||||
// notifies every watcher that this object teleported.
|
||||
_host.Host?.NotifyTeleported();
|
||||
controller.SetBodyOrientation(rotation);
|
||||
|
||||
_cameras.Legacy?.Update(controller.Position, controller.Yaw);
|
||||
_cameras.Retail?.ResetViewerToPlayer(controller.Position, controller.Yaw);
|
||||
_spatial.Reconcile();
|
||||
|
||||
PhysicsDiagnostics.LogTeleport(
|
||||
"PLACED",
|
||||
controller.CellId,
|
||||
$"forced={forced}");
|
||||
Console.WriteLine(
|
||||
$"live: teleport materialized - snapped to {controller.Position} "
|
||||
+ $"cell=0x{controller.CellId:X8}");
|
||||
}
|
||||
|
||||
private Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
|
||||
{
|
||||
int landblockX = (int)((cellId >> 24) & 0xFFu);
|
||||
int landblockY = (int)((cellId >> 16) & 0xFFu);
|
||||
var origin = new Vector3(
|
||||
(landblockX - _origin.CenterX) * 192f,
|
||||
(landblockY - _origin.CenterY) * 192f,
|
||||
0f);
|
||||
return worldPosition - origin;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerTeleportSession
|
||||
{
|
||||
void SendLoginComplete();
|
||||
}
|
||||
|
||||
internal sealed class LocalPlayerTeleportSession : ILocalPlayerTeleportSession
|
||||
{
|
||||
private readonly LiveSessionController _session;
|
||||
|
||||
public LocalPlayerTeleportSession(LiveSessionController session) =>
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
|
||||
public void SendLoginComplete() =>
|
||||
_session.CurrentSession?.SendGameAction(
|
||||
GameActionLoginComplete.Build());
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerTeleportPresentation : IDisposable
|
||||
{
|
||||
bool IsPortalViewportVisible { get; }
|
||||
int CurrentTunnelFrame { get; }
|
||||
void Begin(Matrix4x4 projection);
|
||||
(TeleportAnimSnapshot Snapshot, IReadOnlyList<TeleportAnimEvent> Events)
|
||||
Tick(float deltaSeconds, bool worldReady);
|
||||
void TickTunnel(float deltaSeconds);
|
||||
void EnterTunnel();
|
||||
void ExitTunnel();
|
||||
void 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 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,
|
||||
IDisposable
|
||||
{
|
||||
internal const float MaximumWorldHoldSeconds = 10f;
|
||||
internal const int NearRingRadius =
|
||||
WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius;
|
||||
|
||||
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 bool _forced;
|
||||
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 && originReady && !ready)
|
||||
{
|
||||
_holdSeconds += deltaSeconds;
|
||||
if (_holdSeconds >= MaximumWorldHoldSeconds)
|
||||
{
|
||||
ready = true;
|
||||
_forced = true;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
_forced);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_worldReveal.ObserveMaterialized();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_streaming.ClearPriority();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.EnterTunnel:
|
||||
_presentation.EnterTunnel();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.PlayExitSound:
|
||||
_presentation.ExitTunnel();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.FireLoginComplete:
|
||||
_mode.EnterWorld();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_session.SendLoginComplete();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_worldReveal.Complete();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
ResetTransit(clearSession: false);
|
||||
return;
|
||||
default:
|
||||
// ClientUISystem sound-table enum playback remains outside
|
||||
// this already-accepted presentation extraction.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_presentation.TickTunnel(deltaSeconds);
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ResetTransit(clearSession: true);
|
||||
}
|
||||
|
||||
public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) =>
|
||||
_presentation.ApplyViewPlane(projection);
|
||||
|
||||
public ICamera ApplyViewPlane(ICamera camera) =>
|
||||
_presentation.ApplyViewPlane(camera);
|
||||
|
||||
public void DrawPortalViewport(int width, int height, Matrix4x4 projection) =>
|
||||
_presentation.DrawPortalViewport(width, height, projection);
|
||||
|
||||
private void TryActivatePendingPresentation()
|
||||
{
|
||||
if (!_transit.HasPendingStart)
|
||||
return;
|
||||
|
||||
long generation = _lifetimeGeneration;
|
||||
if (!_mode.TryEnterPortalSpace()
|
||||
|| _lifetimeGeneration != generation
|
||||
|| !_transit.HasPendingStart)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_holdSeconds = 0f;
|
||||
_forced = false;
|
||||
_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}");
|
||||
|
||||
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;
|
||||
_forced = false;
|
||||
_worldReveal.Begin(WorldRevealKind.Portal);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return false;
|
||||
_streaming.SetPriority(
|
||||
StreamingRegion.EncodeLandblockId(landblockX, landblockY),
|
||||
NearRingRadius);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return false;
|
||||
|
||||
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;
|
||||
_forced = false;
|
||||
|
||||
bool originConverged = _streaming.ResetRecenter(clearSession);
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
if (clearSession)
|
||||
{
|
||||
_worldReveal.Cancel();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
}
|
||||
|
||||
_presentation.Reset();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
_streaming.ClearPriority();
|
||||
|
||||
if (clearSession && !originConverged)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The ending session's streaming-origin retirement has not converged.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
44
src/AcDream.App/Streaming/SealedDungeonCellClassifier.cs
Normal file
44
src/AcDream.App/Streaming/SealedDungeonCellClassifier.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
||||
internal interface ISealedDungeonCellClassifier
|
||||
{
|
||||
bool IsSealedDungeon(uint cellId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies a server-supplied full cell ID using the EnvCell's retail
|
||||
/// <see cref="EnvCellFlags.SeenOutside"/> bit. Missing, outdoor, and structural
|
||||
/// shell IDs are never guessed to be sealed.
|
||||
/// </summary>
|
||||
internal sealed class DatSealedDungeonCellClassifier
|
||||
: ISealedDungeonCellClassifier
|
||||
{
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly object _datLock;
|
||||
|
||||
public DatSealedDungeonCellClassifier(
|
||||
IDatReaderWriter dats,
|
||||
object datLock)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
}
|
||||
|
||||
public bool IsSealedDungeon(uint cellId)
|
||||
{
|
||||
uint low = cellId & 0xFFFFu;
|
||||
if (low < 0x0100u || low >= 0xFFFEu)
|
||||
return false;
|
||||
|
||||
EnvCell? envCell;
|
||||
lock (_datLock)
|
||||
envCell = _dats.Get<EnvCell>(cellId);
|
||||
return envCell is not null
|
||||
&& !envCell.Flags.HasFlag(EnvCellFlags.SeenOutside);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue