fix(world): restore portal and distant use lifecycles
This commit is contained in:
parent
c730632075
commit
1a14812c44
18 changed files with 401 additions and 196 deletions
|
|
@ -53,7 +53,9 @@ internal sealed class LocalPlayerControllerSlot
|
|||
/// and completes StopCompletely, so its MotionDone relay must reach the
|
||||
/// candidate interpreter before the complete controller is published.
|
||||
/// </summary>
|
||||
public IDisposable BeginMotionPreparation(PlayerMovementController controller)
|
||||
public IDisposable BeginMotionPreparation(
|
||||
PlayerMovementController controller,
|
||||
Action? drainPriorAnimationQueue = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
if (_preparingMotionOwner is not null)
|
||||
|
|
@ -62,6 +64,12 @@ internal sealed class LocalPlayerControllerSlot
|
|||
"A local player motion owner is already being prepared.");
|
||||
}
|
||||
|
||||
// The live PartArray exists before the local player controller. Drain
|
||||
// any animation completions produced under that prior ownership while
|
||||
// they still resolve to the prior (or null) interpreter. Publishing
|
||||
// the fresh interpreter first would let those old callbacks pop its
|
||||
// new pending_motions queue and leave an unmatched Ready sentinel.
|
||||
drainPriorAnimationQueue?.Invoke();
|
||||
_preparingMotionOwner = controller;
|
||||
return new MotionPreparation(this, controller);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -368,6 +368,7 @@ internal sealed class PlayerModeController :
|
|||
ApplyStepHeights(controller, playerEntity);
|
||||
uint initialCellId = ResolveInitialCell(playerGuid, playerEntity);
|
||||
|
||||
Action? drainPriorAnimationQueue = null;
|
||||
if (_animations.TryGetValue(playerEntity.Id, out LiveEntityAnimationState? animation)
|
||||
&& animation.Sequencer is { } sequencer)
|
||||
{
|
||||
|
|
@ -384,6 +385,7 @@ internal sealed class PlayerModeController :
|
|||
sequencer.Manager.CheckForCompletedMotions;
|
||||
controller.Motion.DefaultSink =
|
||||
new MotionTableDispatchSink(sequencer);
|
||||
drainPriorAnimationQueue = sequencer.Manager.HandleEnterWorld;
|
||||
}
|
||||
|
||||
// Retail CPhysicsObj owns CMotionInterp and CPartArray throughout
|
||||
|
|
@ -393,7 +395,9 @@ internal sealed class PlayerModeController :
|
|||
// public controller slot remains unpublished until every other
|
||||
// player-mode edge has prepared successfully.
|
||||
using IDisposable motionPreparation =
|
||||
_controllerSlot.BeginMotionPreparation(controller);
|
||||
_controllerSlot.BeginMotionPreparation(
|
||||
controller,
|
||||
drainPriorAnimationQueue);
|
||||
|
||||
ResolveResult initial = _physics.Resolve(
|
||||
playerEntity.Position,
|
||||
|
|
|
|||
|
|
@ -24,21 +24,19 @@ internal sealed class SelectionInteractionController
|
|||
private readonly OutboundInteractionQueue _outbound = new();
|
||||
private readonly PlayerApproachCompletionState _approachCompletions;
|
||||
private readonly Action<string>? _toast;
|
||||
private PendingPostArrivalAction? _pendingPostArrival;
|
||||
private PendingPostArrivalPickup? _pendingPostArrival;
|
||||
|
||||
private readonly record struct QueuedInteractionIdentity(
|
||||
uint ServerGuid,
|
||||
uint? LocalEntityId,
|
||||
ClientObject? ClientObject);
|
||||
|
||||
private readonly record struct PendingPostArrivalAction(
|
||||
private readonly record struct PendingPostArrivalPickup(
|
||||
uint ServerGuid,
|
||||
uint LocalEntityId,
|
||||
bool IsPickup,
|
||||
uint DestinationContainerId,
|
||||
int Placement,
|
||||
ulong PendingPlacementToken,
|
||||
ItemUseRequestReservation? UseReservation,
|
||||
PlayerApproachToken ApproachToken);
|
||||
|
||||
public SelectionInteractionController(
|
||||
|
|
@ -238,46 +236,23 @@ internal sealed class SelectionInteractionController
|
|||
return;
|
||||
}
|
||||
|
||||
// ItemHolder::UseObject @ 0x00588A80 sends Event_UseEvent for an
|
||||
// accepted carried-item activation before CPlayerSystem::UsingItem.
|
||||
// A carried source has no world pose to approach: routing it through
|
||||
// TryGetApproach drops the packet because inventory objects are not
|
||||
// live spatial entities.
|
||||
// ItemHolder::UseObject @ 0x00588A80 sends Event_UseEvent before
|
||||
// CPlayerSystem::UsingItem for every accepted ordinary Use. The latter
|
||||
// is presentation/local inventory policy; it never delays the wire
|
||||
// request behind a client-side TurnTo/MoveTo completion. ACE owns the
|
||||
// authoritative approach chain for spatial targets.
|
||||
if (_items.IsOwnedByPlayer(serverGuid))
|
||||
{
|
||||
SendUseNow(serverGuid, reservation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_query.IsUseable(serverGuid)
|
||||
|| !_query.TryGetApproach(serverGuid, out InteractionApproach approach))
|
||||
if (!_query.IsUseable(serverGuid))
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (approach.IsCloseRange)
|
||||
{
|
||||
var pending = new PendingPostArrivalAction(
|
||||
serverGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
IsPickup: false,
|
||||
DestinationContainerId: 0u,
|
||||
Placement: 0,
|
||||
PendingPlacementToken: 0u,
|
||||
UseReservation: reservation,
|
||||
ApproachToken: default);
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
token => _pendingPostArrival = pending with { ApproachToken = token });
|
||||
if (!started && _pendingPostArrival?.ServerGuid == pending.ServerGuid)
|
||||
CancelPendingApproach();
|
||||
else if (!started)
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
_movement.BeginApproach(approach);
|
||||
SendUseNow(serverGuid, reservation);
|
||||
}
|
||||
|
||||
|
|
@ -327,14 +302,12 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
if (approach.IsCloseRange)
|
||||
{
|
||||
var pending = new PendingPostArrivalAction(
|
||||
var pending = new PendingPostArrivalPickup(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
IsPickup: true,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
UseReservation: null,
|
||||
ApproachToken: default);
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
|
|
@ -386,46 +359,33 @@ internal sealed class SelectionInteractionController
|
|||
_pendingPostArrival = null;
|
||||
if (!_query.IsCurrent(pending.ServerGuid, pending.LocalEntityId))
|
||||
{
|
||||
if (pending.IsPickup)
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
else
|
||||
pending.UseReservation?.CancelBeforeDispatch();
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending.IsPickup)
|
||||
if (!IsCurrentPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
pending.PendingPlacementToken))
|
||||
{
|
||||
if (!IsCurrentPickupPresentation(
|
||||
return;
|
||||
}
|
||||
if (!_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
pending.ServerGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
pending.PendingPlacementToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
pending.ServerGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
out _),
|
||||
pending.PendingPlacementToken))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
out _),
|
||||
pending.PendingPlacementToken))
|
||||
{
|
||||
if (_transport.TrySendUse(pending.ServerGuid, out _))
|
||||
pending.UseReservation?.MarkDispatched();
|
||||
else
|
||||
pending.UseReservation?.CancelBeforeDispatch();
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -548,12 +508,9 @@ internal sealed class SelectionInteractionController
|
|||
if (_pendingPostArrival is not { } pending)
|
||||
return;
|
||||
_pendingPostArrival = null;
|
||||
if (pending.IsPickup)
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
else
|
||||
pending.UseReservation?.CancelBeforeDispatch();
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
}
|
||||
|
||||
private void CancelPickupPresentation(uint itemGuid, ulong token = 0u)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,12 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(animation);
|
||||
LiveEntityRuntime runtime = _liveEntities;
|
||||
if (!runtime.IsCurrentSpatialAnimation(record, animation)
|
||||
// The PartArray completion relay is a logical CPhysicsObj component,
|
||||
// not a render-workset component. Projection hydration can bind it
|
||||
// while world rendering is intentionally quiesced during login or
|
||||
// teleport; waiting for spatial visibility would orphan the initial
|
||||
// StopCompletely pending-motion node before the first visible frame.
|
||||
if (!runtime.IsCurrentAnimationOwner(record, animation)
|
||||
|| !ReferenceEquals(record.WorldEntity, animation.Entity)
|
||||
|| animation.Sequencer is not { MotionDoneTarget: null } sequencer)
|
||||
{
|
||||
|
|
@ -55,7 +60,12 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
sequencer.MotionDoneTarget = (motion, success) =>
|
||||
{
|
||||
LiveEntityRuntime current = _liveEntities;
|
||||
if (!current.IsCurrentSpatialAnimation(record, animation)
|
||||
// CPhysicsObj owns its PartArray and CMotionInterp across
|
||||
// leave/enter-world projection edges. The callback may therefore
|
||||
// complete while rendering is intentionally withheld (login or
|
||||
// teleport reveal), but it must never cross an entity incarnation
|
||||
// or an animation-component replacement.
|
||||
if (!current.IsCurrentAnimationOwner(record, animation)
|
||||
|| !ReferenceEquals(record.WorldEntity, capturedEntity)
|
||||
|| !ReferenceEquals(animation.Entity, capturedEntity))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ internal sealed class WorldRevealCoordinator
|
|||
private readonly IWorldRevealStreamingScheduler? _streaming;
|
||||
private readonly IWorldRevealRenderResourceScheduler? _renderResources;
|
||||
private long _activeGeneration;
|
||||
private bool _worldSimulationReleased;
|
||||
private bool _worldViewportReleased;
|
||||
|
||||
public WorldRevealCoordinator(
|
||||
|
|
@ -80,6 +81,7 @@ internal sealed class WorldRevealCoordinator
|
|||
_readiness.Begin();
|
||||
long generation = _lifecycle.Begin(kind);
|
||||
_activeGeneration = generation;
|
||||
_worldSimulationReleased = false;
|
||||
_worldViewportReleased = false;
|
||||
_quiescence?.Begin(generation);
|
||||
_streaming?.BeginDestinationReservation(
|
||||
|
|
@ -107,7 +109,22 @@ internal sealed class WorldRevealCoordinator
|
|||
return snapshot;
|
||||
}
|
||||
|
||||
public void ObserveMaterialized() => _lifecycle.ObserveMaterialized();
|
||||
/// <summary>
|
||||
/// Commits the accepted destination and resumes its hidden world
|
||||
/// simulation while the private portal viewport remains in front.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail <c>SmartBox::UseTime @ 0x00455410</c> resumes
|
||||
/// <c>CObjectMaint</c>/<c>CPhysics</c> before
|
||||
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c> enters
|
||||
/// <c>TAS_TUNNEL_CONTINUE</c>. The normal world viewport is not restored
|
||||
/// until the later <c>TUNNEL_FADE_OUT -> WORLD_FADE_IN</c> edge.
|
||||
/// </remarks>
|
||||
public void ObserveMaterialized()
|
||||
{
|
||||
_lifecycle.ObserveMaterialized();
|
||||
ReleaseWorldSimulation();
|
||||
}
|
||||
|
||||
public void ObserveWorldViewportVisible() =>
|
||||
_lifecycle.ObserveWorldViewportVisible();
|
||||
|
|
@ -116,9 +133,10 @@ internal sealed class WorldRevealCoordinator
|
|||
_lifecycle.ObserveWait(elapsed);
|
||||
|
||||
/// <summary>
|
||||
/// Reopens the destination world at retail's TunnelFadeOut ->
|
||||
/// WorldFadeIn viewport swap without completing the one-second
|
||||
/// WorldFadeIn/LoginComplete tail.
|
||||
/// Releases the destination streaming/resource reservation at retail's
|
||||
/// TunnelFadeOut -> WorldFadeIn viewport swap without completing the
|
||||
/// one-second WorldFadeIn/LoginComplete tail. World simulation already
|
||||
/// resumed when the accepted destination materialized.
|
||||
/// </summary>
|
||||
public void RevealWorldViewport()
|
||||
{
|
||||
|
|
@ -128,7 +146,6 @@ internal sealed class WorldRevealCoordinator
|
|||
|
||||
_streaming?.EndDestinationReservation(generation);
|
||||
_renderResources?.EndDestinationReveal(generation);
|
||||
_quiescence?.End(generation);
|
||||
_worldViewportReleased = true;
|
||||
}
|
||||
|
||||
|
|
@ -150,8 +167,20 @@ internal sealed class WorldRevealCoordinator
|
|||
if (generation == 0)
|
||||
return;
|
||||
|
||||
ReleaseWorldSimulation();
|
||||
RevealWorldViewport();
|
||||
_activeGeneration = 0;
|
||||
_worldSimulationReleased = false;
|
||||
_worldViewportReleased = false;
|
||||
}
|
||||
|
||||
private void ReleaseWorldSimulation()
|
||||
{
|
||||
long generation = _activeGeneration;
|
||||
if (generation == 0 || _worldSimulationReleased)
|
||||
return;
|
||||
|
||||
_quiescence?.End(generation);
|
||||
_worldSimulationReleased = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1901,6 +1901,18 @@ public sealed class LiveEntityRuntime
|
|||
&& _spatialAnimationsByLocalId.TryGetValue(entity.Id, out var indexed)
|
||||
&& ReferenceEquals(indexed, animation);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether <paramref name="animation"/> is still the canonical
|
||||
/// animation component of this logical entity incarnation. Motion
|
||||
/// completion belongs to that CPhysicsObj/PartArray lifetime, not to the
|
||||
/// transient render-bucket projection used by per-frame presentation.
|
||||
/// </summary>
|
||||
internal bool IsCurrentAnimationOwner(
|
||||
LiveEntityRecord record,
|
||||
ILiveEntityAnimationRuntime animation) =>
|
||||
IsCurrentRecord(record)
|
||||
&& ReferenceEquals(record.AnimationRuntime, animation);
|
||||
|
||||
/// <summary>
|
||||
/// Copies the currently spatial animation owners through the concrete
|
||||
/// dictionary enumerator. This keeps per-frame traversal allocation-free;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue