fix(runtime): restore interaction completion ownership
Preserve prepublication local motion completion, require the PartArray enter-world lifecycle port, and balance deferred Use busy ownership across dispatch and cancellation. Reconcile the completed GameWindow connected gates and add regression coverage. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
5c955c36ec
commit
f9736ece6c
26 changed files with 675 additions and 68 deletions
|
|
@ -287,7 +287,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
return new ItemInteractionController(
|
||||
d.Objects,
|
||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
sendUse: selection.SendUse,
|
||||
sendUse: null,
|
||||
sendExamine: guid => session.CurrentSession?.SendAppraise(guid),
|
||||
sendUseWithTarget: (source, target) =>
|
||||
session.CurrentSession?.SendUseWithTarget(source, target),
|
||||
|
|
@ -332,7 +332,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
requestExternalContainer: guid =>
|
||||
{
|
||||
d.ExternalContainers.RequestOpen(guid);
|
||||
});
|
||||
},
|
||||
requestUse: selection.RequestUse);
|
||||
}
|
||||
|
||||
public RetainedUiComposition CreateRetainedUi(
|
||||
|
|
|
|||
|
|
@ -169,6 +169,18 @@ internal sealed class DeferredSelectionUiAuthority
|
|||
target?.SendUse(guid);
|
||||
}
|
||||
|
||||
public void RequestUse(uint guid, ItemUseRequestReservation reservation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reservation);
|
||||
SelectionInteractionController? target;
|
||||
lock (_gate)
|
||||
target = !_deactivated ? _interactions : null;
|
||||
if (target is null)
|
||||
reservation.CancelBeforeDispatch();
|
||||
else
|
||||
target.RequestUse(guid, reservation);
|
||||
}
|
||||
|
||||
public void SendPickup(uint itemGuid, uint destinationContainerId, int placement)
|
||||
{
|
||||
SelectionInteractionController? target;
|
||||
|
|
|
|||
|
|
@ -460,10 +460,11 @@ internal sealed class LivePresentationCompositionPhase
|
|||
liveEntities,
|
||||
d.PhysicsEngine.ShadowObjects,
|
||||
entityEffects.PlayTypedFromHiddenTransition,
|
||||
new LiveEntityPartArrayEnterWorldPort(
|
||||
partArrayLifecycle.HandleEnterWorld),
|
||||
equippedLease.Resource.SetDirectChildrenNoDraw,
|
||||
d.MotionBindings.ClearTargetForHiddenEntity,
|
||||
d.WorldOrigin.GetCenter,
|
||||
partArrayLifecycle.HandleEnterWorld),
|
||||
d.WorldOrigin.GetCenter),
|
||||
static value => value.Dispose());
|
||||
var remoteShadowPlacement = new RemoteShadowPlacementSynchronizer(
|
||||
d.RemotePhysicsUpdater,
|
||||
|
|
|
|||
|
|
@ -21,13 +21,74 @@ internal interface ILocalPlayerControllerSource
|
|||
PlayerMovementController? Controller { get; }
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerMotionSource
|
||||
{
|
||||
MotionInterpreter? Motion { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The one mutable local movement-controller slot. Player-mode lifecycle owns
|
||||
/// assignment; update and presentation owners receive the read-only seam.
|
||||
/// </summary>
|
||||
internal sealed class LocalPlayerControllerSlot : ILocalPlayerControllerSource
|
||||
internal sealed class LocalPlayerControllerSlot
|
||||
: ILocalPlayerControllerSource,
|
||||
ILocalPlayerMotionSource
|
||||
{
|
||||
private PlayerMovementController? _preparingMotionOwner;
|
||||
|
||||
public PlayerMovementController? Controller { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The motion owner visible to the local PartArray completion relay.
|
||||
/// During player-mode construction this is the fully animation-bound
|
||||
/// candidate controller; all other consumers continue to see only the
|
||||
/// committed <see cref="Controller"/>.
|
||||
/// </summary>
|
||||
MotionInterpreter? ILocalPlayerMotionSource.Motion =>
|
||||
_preparingMotionOwner?.Motion ?? Controller?.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the narrow construction-time ownership seam required by retail's
|
||||
/// CPhysicsObj/PartArray lifetime. Initial placement synchronously queues
|
||||
/// and completes StopCompletely, so its MotionDone relay must reach the
|
||||
/// candidate interpreter before the complete controller is published.
|
||||
/// </summary>
|
||||
public IDisposable BeginMotionPreparation(PlayerMovementController controller)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
if (_preparingMotionOwner is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A local player motion owner is already being prepared.");
|
||||
}
|
||||
|
||||
_preparingMotionOwner = controller;
|
||||
return new MotionPreparation(this, controller);
|
||||
}
|
||||
|
||||
private void EndMotionPreparation(PlayerMovementController controller)
|
||||
{
|
||||
if (!ReferenceEquals(_preparingMotionOwner, controller))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The local player motion preparation owner changed unexpectedly.");
|
||||
}
|
||||
|
||||
_preparingMotionOwner = null;
|
||||
}
|
||||
|
||||
private sealed class MotionPreparation(
|
||||
LocalPlayerControllerSlot owner,
|
||||
PlayerMovementController controller) : IDisposable
|
||||
{
|
||||
private LocalPlayerControllerSlot? _owner = owner;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LocalPlayerControllerSlot? current = Interlocked.Exchange(ref _owner, null);
|
||||
current?.EndMotionPreparation(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerPhysicsHostSource
|
||||
|
|
|
|||
|
|
@ -386,6 +386,15 @@ internal sealed class PlayerModeController :
|
|||
new MotionTableDispatchSink(sequencer);
|
||||
}
|
||||
|
||||
// Retail CPhysicsObj owns CMotionInterp and CPartArray throughout
|
||||
// construction. Our split owners preserve that lifetime with a
|
||||
// narrow preparation lease: SetPosition's synchronous type-5
|
||||
// completion reaches this candidate MotionInterpreter, while the
|
||||
// public controller slot remains unpublished until every other
|
||||
// player-mode edge has prepared successfully.
|
||||
using IDisposable motionPreparation =
|
||||
_controllerSlot.BeginMotionPreparation(controller);
|
||||
|
||||
ResolveResult initial = _physics.Resolve(
|
||||
playerEntity.Position,
|
||||
initialCellId,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ internal sealed class SelectionInteractionController
|
|||
uint DestinationContainerId,
|
||||
int Placement,
|
||||
ulong PendingPlacementToken,
|
||||
ItemUseRequestReservation? UseReservation,
|
||||
PlayerApproachToken ApproachToken);
|
||||
|
||||
public SelectionInteractionController(
|
||||
|
|
@ -197,16 +198,23 @@ internal sealed class SelectionInteractionController
|
|||
}
|
||||
|
||||
public void SendUse(uint serverGuid)
|
||||
=> RequestUse(serverGuid, reservation: null);
|
||||
|
||||
public void RequestUse(
|
||||
uint serverGuid,
|
||||
ItemUseRequestReservation? reservation)
|
||||
{
|
||||
CancelPendingApproach();
|
||||
if (!_transport.IsInWorld)
|
||||
{
|
||||
_toast?.Invoke("Not in world");
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
if (!_query.IsUseable(serverGuid)
|
||||
|| !_query.TryGetApproach(serverGuid, out InteractionApproach approach))
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -219,18 +227,28 @@ internal sealed class SelectionInteractionController
|
|||
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);
|
||||
if (_transport.TrySendUse(serverGuid, out uint sequence))
|
||||
{
|
||||
reservation?.MarkDispatched();
|
||||
Console.WriteLine($"[B.4b] use guid=0x{serverGuid:X8} seq={sequence}");
|
||||
}
|
||||
else
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
}
|
||||
}
|
||||
|
||||
public void SendPickup(uint itemGuid, uint destinationContainerId, int placement)
|
||||
|
|
@ -272,6 +290,7 @@ internal sealed class SelectionInteractionController
|
|||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
UseReservation: null,
|
||||
ApproachToken: default);
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
|
|
@ -327,6 +346,8 @@ internal sealed class SelectionInteractionController
|
|||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
else
|
||||
pending.UseReservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -357,7 +378,10 @@ internal sealed class SelectionInteractionController
|
|||
}
|
||||
else
|
||||
{
|
||||
_transport.TrySendUse(pending.ServerGuid, out _);
|
||||
if (_transport.TrySendUse(pending.ServerGuid, out _))
|
||||
pending.UseReservation?.MarkDispatched();
|
||||
else
|
||||
pending.UseReservation?.CancelBeforeDispatch();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -484,6 +508,8 @@ internal sealed class SelectionInteractionController
|
|||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
else
|
||||
pending.UseReservation?.CancelBeforeDispatch();
|
||||
}
|
||||
|
||||
private void CancelPickupPresentation(uint itemGuid, ulong token = 0u)
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ internal sealed class LiveAnimationPresentationContext
|
|||
{
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly ILocalPlayerIdentitySource _localPlayer;
|
||||
private readonly ILocalPlayerControllerSource _playerController;
|
||||
private readonly ILocalPlayerMotionSource _playerMotion;
|
||||
|
||||
public LiveAnimationPresentationContext(
|
||||
LiveEntityRuntime runtime,
|
||||
ILocalPlayerIdentitySource localPlayer,
|
||||
ILocalPlayerControllerSource playerController)
|
||||
ILocalPlayerMotionSource playerMotion)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_localPlayer = localPlayer ?? throw new ArgumentNullException(nameof(localPlayer));
|
||||
_playerController = playerController
|
||||
?? throw new ArgumentNullException(nameof(playerController));
|
||||
_playerMotion = playerMotion
|
||||
?? throw new ArgumentNullException(nameof(playerMotion));
|
||||
}
|
||||
|
||||
public uint LocalPlayerGuid => _localPlayer.ServerGuid;
|
||||
|
|
@ -38,7 +38,7 @@ internal sealed class LiveAnimationPresentationContext
|
|||
}
|
||||
|
||||
if (record.ServerGuid == _localPlayer.ServerGuid)
|
||||
return _playerController.Controller?.Motion;
|
||||
return _playerMotion.Motion;
|
||||
return record.RemoteMotionRuntime is RemoteMotion remote
|
||||
? remote.Motion
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,32 @@ public readonly record struct PendingInventoryRequest(
|
|||
ClientObject? ItemIdentity,
|
||||
bool Dispatched);
|
||||
|
||||
/// <summary>
|
||||
/// Owns one retail UI busy reference while an accepted Use request crosses the
|
||||
/// local approach boundary. Dispatch transfers that reference to the matching
|
||||
/// authoritative UseDone; cancellation before dispatch releases it locally.
|
||||
/// </summary>
|
||||
public sealed class ItemUseRequestReservation
|
||||
{
|
||||
private readonly Action<bool> _resolve;
|
||||
private int _resolved;
|
||||
|
||||
internal ItemUseRequestReservation(Action<bool> resolve)
|
||||
=> _resolve = resolve ?? throw new ArgumentNullException(nameof(resolve));
|
||||
|
||||
public void MarkDispatched()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _resolved, 1) == 0)
|
||||
_resolve(true);
|
||||
}
|
||||
|
||||
public void CancelBeforeDispatch()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _resolved, 1) == 0)
|
||||
_resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared retail item interaction orchestrator. UI widgets route item clicks,
|
||||
/// target acquisition, and drag-out drops here instead of duplicating
|
||||
|
|
@ -78,6 +104,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly Func<bool> _dragOnPlayerOpensSecureTrade;
|
||||
private readonly Action<string>? _systemMessage;
|
||||
private readonly AutoWieldController _autoWield;
|
||||
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
||||
|
||||
private long _lastUseMs = long.MinValue / 2;
|
||||
private uint _consumedPrimaryClickTarget;
|
||||
|
|
@ -86,6 +113,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private ulong _nextInventoryRequestToken;
|
||||
private PendingBackpackPlacement? _pendingBackpackPlacement;
|
||||
private PendingInventoryRequest? _pendingInventoryRequest;
|
||||
private ulong _useReservationGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
public ItemInteractionController(
|
||||
|
|
@ -118,7 +146,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Action<uint, uint, uint, uint>? sendSplitToContainer = null,
|
||||
Action<uint>? requestExternalContainer = null,
|
||||
CombatState? combatState = null,
|
||||
Action<CombatMode>? sendChangeCombatMode = null)
|
||||
Action<CombatMode>? sendChangeCombatMode = null,
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -147,6 +176,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_stackSplitQuantity = stackSplitQuantity;
|
||||
_dragOnPlayerOpensSecureTrade = dragOnPlayerOpensSecureTrade ?? (() => true);
|
||||
_systemMessage = systemMessage;
|
||||
_requestUse = requestUse;
|
||||
_interactionState = interactionState ?? new InteractionState();
|
||||
_interactionState.Changed += OnInteractionModeChanged;
|
||||
_objects.ObjectMoved += OnInventoryObjectMoved;
|
||||
|
|
@ -757,12 +787,28 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// </summary>
|
||||
public bool ExecuteConfirmedUse(uint objectId)
|
||||
{
|
||||
if (objectId == 0u || _sendUse is null)
|
||||
if (objectId == 0u || (_requestUse is null && _sendUse is null))
|
||||
return false;
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
_sendUse(objectId);
|
||||
_busyCount++;
|
||||
if (_requestUse is not null)
|
||||
{
|
||||
ItemUseRequestReservation reservation = BeginUseRequestReservation();
|
||||
try
|
||||
{
|
||||
_requestUse(objectId, reservation);
|
||||
}
|
||||
catch
|
||||
{
|
||||
reservation.CancelBeforeDispatch();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_sendUse!(objectId);
|
||||
_busyCount++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -815,6 +861,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
uint openedOrUsed = 0;
|
||||
bool acted = false;
|
||||
bool busyOwnedByUseReservation = false;
|
||||
foreach (var action in actions)
|
||||
{
|
||||
switch (action.Kind)
|
||||
|
|
@ -835,9 +882,27 @@ public sealed class ItemInteractionController : IDisposable
|
|||
// coalesce the pair to one packet.
|
||||
if (openedOrUsed != action.ObjectId)
|
||||
{
|
||||
_sendUse?.Invoke(action.ObjectId);
|
||||
if (_requestUse is not null)
|
||||
{
|
||||
ItemUseRequestReservation reservation =
|
||||
BeginUseRequestReservation();
|
||||
try
|
||||
{
|
||||
_requestUse(action.ObjectId, reservation);
|
||||
}
|
||||
catch
|
||||
{
|
||||
reservation.CancelBeforeDispatch();
|
||||
throw;
|
||||
}
|
||||
busyOwnedByUseReservation = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sendUse?.Invoke(action.ObjectId);
|
||||
}
|
||||
openedOrUsed = action.ObjectId;
|
||||
acted |= _sendUse is not null;
|
||||
acted |= _requestUse is not null || _sendUse is not null;
|
||||
}
|
||||
break;
|
||||
case ItemPolicyActionKind.SendUseWithTarget:
|
||||
|
|
@ -853,7 +918,14 @@ public sealed class ItemInteractionController : IDisposable
|
|||
acted = true;
|
||||
break;
|
||||
case ItemPolicyActionKind.IncrementBusy:
|
||||
_busyCount++;
|
||||
if (busyOwnedByUseReservation)
|
||||
{
|
||||
busyOwnedByUseReservation = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_busyCount++;
|
||||
}
|
||||
break;
|
||||
case ItemPolicyActionKind.Reject:
|
||||
if (!string.IsNullOrWhiteSpace(action.Message))
|
||||
|
|
@ -872,6 +944,21 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return acted;
|
||||
}
|
||||
|
||||
private ItemUseRequestReservation BeginUseRequestReservation()
|
||||
{
|
||||
ulong generation = _useReservationGeneration;
|
||||
_busyCount++;
|
||||
StateChanged?.Invoke();
|
||||
return new ItemUseRequestReservation(dispatched =>
|
||||
{
|
||||
if (generation != _useReservationGeneration || dispatched)
|
||||
return;
|
||||
if (_busyCount > 0)
|
||||
_busyCount--;
|
||||
StateChanged?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private void ExecutePlacementActions(System.Collections.Generic.IReadOnlyList<ItemPolicyAction> actions)
|
||||
{
|
||||
foreach (var action in actions)
|
||||
|
|
@ -1120,6 +1207,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
public void ClearBusy()
|
||||
{
|
||||
if (_busyCount == 0) return;
|
||||
_useReservationGeneration++;
|
||||
_busyCount = 0;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
|
@ -1133,6 +1221,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
PendingBackpackPlacement? pendingPlacement = _pendingBackpackPlacement;
|
||||
_pendingBackpackPlacement = null;
|
||||
_useReservationGeneration++;
|
||||
_busyCount = 0;
|
||||
_pendingInventoryRequest = null;
|
||||
_lastUseMs = long.MinValue / 2;
|
||||
|
|
|
|||
22
src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs
Normal file
22
src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Strongly typed retail PartArray world-entry boundary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>CPhysicsObj::set_hidden @ 0x00514C60</c> invokes
|
||||
/// <c>CPartArray::HandleEnterWorld @ 0x00517D70</c> on both Hidden edges.
|
||||
/// This dedicated port prevents that mandatory lifecycle callback from being
|
||||
/// confused with neighboring <see cref="Action{T}"/> presentation callbacks.
|
||||
/// </remarks>
|
||||
public sealed class LiveEntityPartArrayEnterWorldPort
|
||||
{
|
||||
private readonly Action<uint> _handleEnterWorld;
|
||||
|
||||
public LiveEntityPartArrayEnterWorldPort(Action<uint> handleEnterWorld) =>
|
||||
_handleEnterWorld = handleEnterWorld
|
||||
?? throw new ArgumentNullException(nameof(handleEnterWorld));
|
||||
|
||||
public void HandleEnterWorld(uint localEntityId) =>
|
||||
_handleEnterWorld(localEntityId);
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
private readonly Action<uint> _clearInvalidTarget;
|
||||
private readonly Func<(int X, int Y)> _liveCenter;
|
||||
private readonly Action<uint>? _onShadowRestored;
|
||||
private readonly Action<uint> _handlePartArrayEnterWorld;
|
||||
private readonly LiveEntityPartArrayEnterWorldPort _partArrayEnterWorld;
|
||||
private readonly Dictionary<uint, ushort> _readyGenerationByGuid = new();
|
||||
private readonly Dictionary<uint, ushort> _suspendedShadowGenerationByGuid = new();
|
||||
private readonly Dictionary<uint, ushort> _activePlacementGenerationByGuid = new();
|
||||
|
|
@ -38,20 +38,21 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
LiveEntityRuntime liveEntities,
|
||||
ShadowObjectRegistry shadows,
|
||||
Func<uint, uint, float, bool> playTyped,
|
||||
LiveEntityPartArrayEnterWorldPort partArrayEnterWorld,
|
||||
Action<uint, bool>? setDirectChildrenNoDraw = null,
|
||||
Action<uint>? clearInvalidTarget = null,
|
||||
Func<(int X, int Y)>? liveCenter = null,
|
||||
Action<uint>? onShadowRestored = null,
|
||||
Action<uint>? handlePartArrayEnterWorld = null)
|
||||
Action<uint>? onShadowRestored = null)
|
||||
{
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
||||
_playTyped = playTyped ?? throw new ArgumentNullException(nameof(playTyped));
|
||||
_partArrayEnterWorld = partArrayEnterWorld
|
||||
?? throw new ArgumentNullException(nameof(partArrayEnterWorld));
|
||||
_setDirectChildrenNoDraw = setDirectChildrenNoDraw ?? ((_, _) => { });
|
||||
_clearInvalidTarget = clearInvalidTarget ?? (_ => { });
|
||||
_liveCenter = liveCenter ?? (() => (0, 0));
|
||||
_onShadowRestored = onShadowRestored;
|
||||
_handlePartArrayEnterWorld = handlePartArrayEnterWorld ?? (_ => { });
|
||||
_liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged;
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +263,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
// timeline boundary: it strips link animations and
|
||||
// aborts every pending completion through
|
||||
// MotionTableManager::HandleEnterWorld @ 0x0051BDD0.
|
||||
_handlePartArrayEnterWorld(entity.Id);
|
||||
_partArrayEnterWorld.HandleEnterWorld(entity.Id);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
_clearInvalidTarget(record.ServerGuid);
|
||||
|
|
@ -279,7 +280,7 @@ public sealed class LiveEntityPresentationController : IDisposable
|
|||
return false;
|
||||
// Retail invokes the same PartArray boundary before
|
||||
// CObjCell::unhide_object restores cell visibility.
|
||||
_handlePartArrayEnterWorld(entity.Id);
|
||||
_partArrayEnterWorld.HandleEnterWorld(entity.Id);
|
||||
if (!IsCurrent(record, entity))
|
||||
return false;
|
||||
bool restored = !IsPlacementActive(record)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue