fix(interaction): restore retail loot placement and world-drop projection
This commit is contained in:
parent
4d095be286
commit
921712f412
24 changed files with 1581 additions and 34 deletions
|
|
@ -503,6 +503,17 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
live.LandblockLoaded.Bind(hydration));
|
||||
Fault(SessionPlayerCompositionPoint.HydrationCreated);
|
||||
|
||||
var worldDropProjection =
|
||||
new InventoryWorldDropProjectionController(
|
||||
interaction.ItemInteraction,
|
||||
d.EntityObjects.Objects,
|
||||
live.LiveEntities,
|
||||
hydration,
|
||||
() => d.UpdateClock.SimulationTimeSeconds);
|
||||
bindings.Adopt(
|
||||
"inventory world-drop projection",
|
||||
worldDropProjection);
|
||||
|
||||
var networkUpdates = new LiveEntityNetworkUpdateController(
|
||||
live.LiveEntities,
|
||||
d.EntityObjects.Objects,
|
||||
|
|
@ -531,7 +542,8 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.UpdateClock,
|
||||
liveSessionSource,
|
||||
localPhysicsTimestamps.Publish,
|
||||
d.MovementDiagnostics);
|
||||
d.MovementDiagnostics,
|
||||
worldDropProjection);
|
||||
var liveness = new LiveEntityLivenessController(
|
||||
live.LiveEntities,
|
||||
d.PlayerIdentity,
|
||||
|
|
|
|||
|
|
@ -230,12 +230,6 @@ internal sealed class SelectionInteractionController
|
|||
CancelPickupPresentation(itemGuid);
|
||||
return;
|
||||
}
|
||||
if (!ValidatePickupTarget(itemGuid, showToast: true)
|
||||
|| !_query.TryGetApproach(itemGuid, out InteractionApproach approach))
|
||||
{
|
||||
CancelPickupPresentation(itemGuid);
|
||||
return;
|
||||
}
|
||||
|
||||
ulong pendingPlacementToken = _items.TryGetPendingBackpackPlacement(
|
||||
itemGuid,
|
||||
|
|
@ -251,6 +245,42 @@ internal sealed class SelectionInteractionController
|
|||
return;
|
||||
}
|
||||
|
||||
// Retail CPlayerSystem::PlaceInBackpack @ 0x0055D8C0 delegates
|
||||
// current-ground-object contents straight to
|
||||
// ItemHolder::AttemptToPlaceInContainer @ 0x00588140. A corpse or
|
||||
// chest child has no independent 3-D projection to approach.
|
||||
if (_items.IsInCurrentGroundObject(itemGuid))
|
||||
{
|
||||
var contained = new RuntimePendingPickup(
|
||||
Token: 0u,
|
||||
itemGuid,
|
||||
LocalEntityId: 0u,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
ApproachToken: default);
|
||||
if (_transactions.TryDispatchPickup(
|
||||
contained,
|
||||
_transport,
|
||||
out uint containedSequence))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[B.5] contained pickup item=0x{itemGuid:X8} container=0x{destinationContainerId:X8} placement={placement} seq={containedSequence}");
|
||||
}
|
||||
else
|
||||
{
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidatePickupTarget(itemGuid, showToast: true)
|
||||
|| !_query.TryGetApproach(itemGuid, out InteractionApproach approach))
|
||||
{
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (approach.IsCloseRange)
|
||||
{
|
||||
bool armed = false;
|
||||
|
|
|
|||
|
|
@ -261,7 +261,19 @@ internal sealed class WorldSelectionQuery
|
|||
|
||||
public VividTargetInfo? ResolveVividTargetInfo(uint serverGuid)
|
||||
{
|
||||
if (!_liveEntities.TryGetWorldEntity(serverGuid, out _)
|
||||
uint playerGuid = _playerGuid();
|
||||
ClientObject? target = _objects.Get(serverGuid);
|
||||
|
||||
// VividTargetIndicator::SetSelected @ 0x004F5CE0 deliberately keeps
|
||||
// ACCWeenieObject::selectedID intact while suppressing its own marker
|
||||
// for self, player-owned objects, and objects whose current state is
|
||||
// IN_CONTAINER. ContainerId is our authoritative placement projection
|
||||
// for that retail state.
|
||||
if (serverGuid == playerGuid
|
||||
|| target is null
|
||||
|| _objects.IsOwnedByObject(serverGuid, playerGuid)
|
||||
|| target.ContainerId != 0u
|
||||
|| !_liveEntities.TryGetSpatiallyProjectedRecord(serverGuid, out _)
|
||||
|| !TryGetSelectionSphere(serverGuid, out Vector3 center, out float radius))
|
||||
{
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ internal readonly record struct AcceptedStateNetworkUpdate(
|
|||
|
||||
internal readonly record struct AcceptedPositionNetworkUpdate(
|
||||
WorldSession.EntityPositionUpdate Update,
|
||||
LiveEntityRecord Record,
|
||||
RuntimeEntityRecord Canonical,
|
||||
WorldSession.EntitySpawn Spawn,
|
||||
AcceptedPhysicsTimestamps Timestamps,
|
||||
PositionTimestampDisposition TimestampDisposition,
|
||||
|
|
@ -173,22 +173,22 @@ internal sealed class LiveEntityInboundAuthorityGate
|
|||
return false;
|
||||
}
|
||||
|
||||
LiveEntityRecord? record = null;
|
||||
RuntimeEntityRecord? canonical = null;
|
||||
ulong positionAuthorityVersion = 0;
|
||||
ulong velocityAuthorityVersion = 0;
|
||||
if (disposition is not PositionTimestampDisposition.Rejected)
|
||||
{
|
||||
if (!_liveEntities.TryGetRecord(update.Guid, out record))
|
||||
if (!_liveEntities.TryGetCanonical(update.Guid, out canonical))
|
||||
return false;
|
||||
positionAuthorityVersion = record.PositionAuthorityVersion;
|
||||
velocityAuthorityVersion = record.VelocityAuthorityVersion;
|
||||
positionAuthorityVersion = canonical.PositionAuthorityVersion;
|
||||
velocityAuthorityVersion = canonical.VelocityAuthorityVersion;
|
||||
}
|
||||
|
||||
_publishTimestamps(update.Guid, timestamps);
|
||||
if (disposition is PositionTimestampDisposition.Rejected)
|
||||
return false;
|
||||
if (!_liveEntities.IsCurrentPositionAuthority(
|
||||
record!,
|
||||
canonical!,
|
||||
positionAuthorityVersion))
|
||||
{
|
||||
return false;
|
||||
|
|
@ -196,7 +196,7 @@ internal sealed class LiveEntityInboundAuthorityGate
|
|||
|
||||
accepted = new AcceptedPositionNetworkUpdate(
|
||||
update,
|
||||
record!,
|
||||
canonical!,
|
||||
spawn,
|
||||
timestamps,
|
||||
disposition,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
private readonly ILiveWorldSessionSource _session;
|
||||
private readonly LiveEntityInboundAuthorityGate _authorityGate;
|
||||
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
|
||||
private readonly InventoryWorldDropProjectionController?
|
||||
_worldDropProjection;
|
||||
|
||||
private PlayerMovementController? _playerController => _playerControllerSource.Controller;
|
||||
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
|
||||
|
|
@ -100,7 +102,8 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
IPhysicsScriptTimeSource gameTime,
|
||||
ILiveWorldSessionSource session,
|
||||
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
||||
IMovementTruthDiagnosticSink movementTruthDiagnostics)
|
||||
IMovementTruthDiagnosticSink movementTruthDiagnostics,
|
||||
InventoryWorldDropProjectionController? worldDropProjection = null)
|
||||
{
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
|
|
@ -135,6 +138,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
publishTimestamps);
|
||||
_movementTruthDiagnostics = movementTruthDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(movementTruthDiagnostics));
|
||||
_worldDropProjection = worldDropProjection;
|
||||
}
|
||||
|
||||
internal void ResetSessionState() => _authorityGate.ResetSessionState();
|
||||
|
|
@ -936,6 +940,11 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
|
||||
public void OnPosition(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
if (_worldDropProjection?.TryRecoverUnknownPosition(update) == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool payloadIsValid = _projectileController?.CanAcceptPositionPayload(
|
||||
update.Guid,
|
||||
update.Position,
|
||||
|
|
@ -957,11 +966,22 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
var timestampDisposition = accepted.TimestampDisposition;
|
||||
var acceptedSpawn = accepted.Spawn;
|
||||
var timestamps = accepted.Timestamps;
|
||||
LiveEntityRecord acceptedPositionRecord = accepted.Record;
|
||||
RuntimeEntityRecord acceptedPositionCanonical = accepted.Canonical;
|
||||
ulong acceptedPositionAuthorityVersion =
|
||||
accepted.PositionAuthorityVersion;
|
||||
ulong acceptedPositionVelocityAuthorityVersion =
|
||||
accepted.VelocityAuthorityVersion;
|
||||
if (!_liveEntities.TryGetProjection(
|
||||
acceptedPositionCanonical,
|
||||
out LiveEntityRecord acceptedPositionRecord)
|
||||
&& !_liveEntityHydration.RecoverCanonicalProjection(
|
||||
acceptedPositionCanonical,
|
||||
acceptedPositionAuthorityVersion,
|
||||
out acceptedPositionRecord))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool IsCurrentPositionOwner(
|
||||
AcDream.Core.World.WorldEntity? expectedEntity = null) =>
|
||||
_liveEntities.IsCurrentPositionAuthority(
|
||||
|
|
@ -1013,7 +1033,12 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
_playerController)))
|
||||
return;
|
||||
|
||||
if (!_liveEntities.ContainsWorldEntity(update.Guid))
|
||||
// A leave-world transition deliberately retains WorldEntity as the
|
||||
// logical/render-resource owner while IsSpatiallyProjected is false.
|
||||
// A fresh retail Position is the re-entry edge; testing only for the
|
||||
// retained object reference leaves dropped inventory permanently
|
||||
// invisible after InventoryPutObjectIn3D.
|
||||
if (RequiresSpatialProjectionRecovery(acceptedPositionRecord))
|
||||
{
|
||||
if (!IsCurrentPositionOwner())
|
||||
return;
|
||||
|
|
@ -1771,6 +1796,13 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
}
|
||||
}
|
||||
|
||||
internal static bool RequiresSpatialProjectionRecovery(
|
||||
LiveEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
return record.WorldEntity is null || !record.IsSpatiallyProjected;
|
||||
}
|
||||
|
||||
// Retail teleport transit: the 7-state TAS drives portal/view-plane presentation, holds the
|
||||
// player in PortalSpace until the destination is resident (TeleportWorldReady), then
|
||||
// fires Place (materialize) and FireLoginComplete (regain control + ack the server).
|
||||
|
|
|
|||
|
|
@ -993,7 +993,13 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
|
||||
private void OnObjectMoved(ClientObjectMove move)
|
||||
{
|
||||
if (move.Current.EquipLocation == EquipMask.None)
|
||||
// ServerSaysMoveItem publishes both complete placements. Only an
|
||||
// equipped -> unequipped transition retires an attached paperdoll
|
||||
// projection. InventoryPutObjectIn3D also has EquipLocation=None, but
|
||||
// it is a world-entry confirmation and must never withdraw a Position
|
||||
// projection that arrived just before the F019A event.
|
||||
if (move.Previous.EquipLocation != EquipMask.None
|
||||
&& move.Current.EquipLocation == EquipMask.None)
|
||||
BeginDetachedRemoval(move.ItemId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,14 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// </summary>
|
||||
public event Action<PendingBackpackPlacement>? PendingBackpackPlacementResolved;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the exact split-to-world request after its wire send has
|
||||
/// succeeded. Retail <c>ACCWeenieObject::UIAttemptSplitTo3D @
|
||||
/// 0x0058D850</c> retains the source class, selected amount, and request
|
||||
/// time so the subsequently created ground stack can be recognized.
|
||||
/// </summary>
|
||||
public event Action<WorldDropDispatch>? WorldDropDispatched;
|
||||
|
||||
public uint PlayerGuid => _playerGuid();
|
||||
|
||||
public InteractionState InteractionState => _interactionState;
|
||||
|
|
@ -302,14 +310,10 @@ public sealed class ItemInteractionController : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Retail <c>ACCWeenieObject::IsOwnedByPlayer</c> projection shared with
|
||||
/// toolbar shortcut creation. Ownership includes self, equipped items, and
|
||||
/// arbitrarily nested carried containers.
|
||||
/// toolbar shortcut creation.
|
||||
/// </summary>
|
||||
public bool IsOwnedByPlayer(uint itemGuid)
|
||||
=> _objects.Get(itemGuid) is { } item
|
||||
&& (item.ObjectId == _playerGuid()
|
||||
|| IsCarriedByPlayer(item)
|
||||
|| IsEquippedByPlayer(item));
|
||||
=> _objects.IsOwnedByObject(itemGuid, _playerGuid());
|
||||
|
||||
public bool IsCurrentTargetCompatible(uint targetGuid)
|
||||
{
|
||||
|
|
@ -721,6 +725,21 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return item is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the object is a member of retail's current
|
||||
/// <c>ClientUISystem::groundObject</c>. Such objects are container
|
||||
/// contents, not independently projected world objects, so
|
||||
/// <c>CPlayerSystem::PlaceInBackpack @ 0x0055D8C0</c> sends their
|
||||
/// container transfer directly without a 3-D approach lookup.
|
||||
/// </summary>
|
||||
internal bool IsInCurrentGroundObject(uint objectId)
|
||||
{
|
||||
uint groundObjectId = _groundObjectId();
|
||||
return groundObjectId != 0u
|
||||
&& _objects.Get(objectId) is { } item
|
||||
&& item.ContainerId == groundObjectId;
|
||||
}
|
||||
|
||||
internal bool IsCurrentObjectIdentity(uint objectId, ClientObject item)
|
||||
=> ReferenceEquals(_objects.Get(objectId), item);
|
||||
|
||||
|
|
@ -983,6 +1002,14 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (_sendSplitToWorld is null)
|
||||
return false;
|
||||
_sendSplitToWorld(action.ObjectId, (uint)action.Amount);
|
||||
if (_transactions.TryGetPending(out PendingInventoryRequest pending)
|
||||
&& pending.Kind is InventoryRequestKind.SplitToWorld
|
||||
&& pending.ItemId == action.ObjectId)
|
||||
{
|
||||
WorldDropDispatched?.Invoke(new WorldDropDispatch(
|
||||
pending,
|
||||
(uint)action.Amount));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
|
|
@ -1137,6 +1164,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_transactions.ObjectTableCleared -= OnInventoryObjectsCleared;
|
||||
_transactions.RequestCompleted -= OnInventoryRequestCompleted;
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
WorldDropDispatched = null;
|
||||
_autoWield.Dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
11
src/AcDream.App/UI/WorldDropDispatch.cs
Normal file
11
src/AcDream.App/UI/WorldDropDispatch.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// One successfully dispatched inventory-to-world request together with the
|
||||
/// quantity selected by the retained UI.
|
||||
/// </summary>
|
||||
public readonly record struct WorldDropDispatch(
|
||||
PendingInventoryRequest Request,
|
||||
uint Amount);
|
||||
216
src/AcDream.App/World/InventoryWorldDropProjectionController.cs
Normal file
216
src/AcDream.App/World/InventoryWorldDropProjectionController.cs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges retail's pending split-to-3D identity into canonical live-entity
|
||||
/// registration when an ACE owner receives the result's Position before a
|
||||
/// CreateObject.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail records <c>splitClassID</c>, <c>splitStackSize</c>, and
|
||||
/// <c>splitTime</c> in <c>ACCWeenieObject::UIAttemptSplitTo3D @
|
||||
/// 0x0058D850</c>, then recognizes the created stack from
|
||||
/// <c>ACCWeenieObject::DeclareValid @ 0x0058E340</c>. ACE currently omits
|
||||
/// CreateObject for the initiating player but sends the new GUID's F748
|
||||
/// Position. This owner accepts only that otherwise-impossible packet while
|
||||
/// the exact retail pending-split identity is live; arbitrary unknown Position
|
||||
/// packets remain rejected by the ordinary authority gate.
|
||||
/// </remarks>
|
||||
internal sealed class InventoryWorldDropProjectionController : IDisposable
|
||||
{
|
||||
private readonly ItemInteractionController _interaction;
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly LiveEntityHydrationController _hydration;
|
||||
private readonly PendingSplitToWorldProjection _pending;
|
||||
private readonly Func<double> _now;
|
||||
private bool _disposed;
|
||||
|
||||
public InventoryWorldDropProjectionController(
|
||||
ItemInteractionController interaction,
|
||||
ClientObjectTable objects,
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityHydrationController hydration,
|
||||
Func<double> now)
|
||||
{
|
||||
_interaction = interaction
|
||||
?? throw new ArgumentNullException(nameof(interaction));
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_hydration = hydration
|
||||
?? throw new ArgumentNullException(nameof(hydration));
|
||||
_now = now ?? throw new ArgumentNullException(nameof(now));
|
||||
_pending = new PendingSplitToWorldProjection();
|
||||
|
||||
_interaction.WorldDropDispatched += OnWorldDropDispatched;
|
||||
_objects.MoveRequestFailed += OnMoveRequestFailed;
|
||||
_objects.Cleared += OnObjectsCleared;
|
||||
}
|
||||
|
||||
public bool TryRecoverUnknownPosition(
|
||||
WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
bool known = _runtime.TryGetSnapshot(update.Guid, out _);
|
||||
if (known
|
||||
|| !_pending.TryResolve(update, _now(), out WorldSession.EntitySpawn spawn))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Consume before the synchronous CreateObject graph runs. Re-entrant
|
||||
// callbacks cannot bind the same split intent to a second GUID.
|
||||
_hydration.OnCreate(spawn);
|
||||
return _runtime.TryGetSnapshot(update.Guid, out _);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_objects.Cleared -= OnObjectsCleared;
|
||||
_objects.MoveRequestFailed -= OnMoveRequestFailed;
|
||||
_interaction.WorldDropDispatched -= OnWorldDropDispatched;
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
private void OnWorldDropDispatched(WorldDropDispatch dispatch)
|
||||
{
|
||||
bool hasSource = _runtime.TryGetSnapshot(
|
||||
dispatch.Request.ItemId,
|
||||
out WorldSession.EntitySpawn source);
|
||||
if (dispatch.Request.Kind is not InventoryRequestKind.SplitToWorld
|
||||
|| dispatch.Amount == 0
|
||||
|| !hasSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pending.Record(
|
||||
dispatch.Request.Token,
|
||||
source,
|
||||
dispatch.Amount,
|
||||
_now());
|
||||
}
|
||||
|
||||
private void OnMoveRequestFailed(MoveRequestFailure failure) =>
|
||||
_pending.ClearSource(failure.ItemId);
|
||||
|
||||
private void OnObjectsCleared() => _pending.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-independent pending identity for retail split-to-3D
|
||||
/// recognition. Retail owns one static split identity and replaces it on the
|
||||
/// next request; this owner intentionally does the same.
|
||||
/// </summary>
|
||||
internal sealed class PendingSplitToWorldProjection
|
||||
{
|
||||
internal const double RetailRecognitionSeconds = 10.0;
|
||||
|
||||
private PendingSplit? _value;
|
||||
|
||||
internal bool HasPending => _value is not null;
|
||||
|
||||
internal void Record(
|
||||
ulong requestToken,
|
||||
WorldSession.EntitySpawn source,
|
||||
uint amount,
|
||||
double now)
|
||||
{
|
||||
if (requestToken == 0
|
||||
|| source.Guid == 0
|
||||
|| amount == 0
|
||||
|| !double.IsFinite(now))
|
||||
{
|
||||
_value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_value = new PendingSplit(requestToken, source, amount, now);
|
||||
}
|
||||
|
||||
internal bool TryResolve(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
double now,
|
||||
out WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
spawn = default;
|
||||
if (_value is not { } pending)
|
||||
return false;
|
||||
|
||||
double age = now - pending.RequestTime;
|
||||
if (!double.IsFinite(age)
|
||||
|| age < 0
|
||||
|| age >= RetailRecognitionSeconds)
|
||||
{
|
||||
_value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (update.Guid == 0 || update.Guid == pending.Source.Guid)
|
||||
return false;
|
||||
|
||||
_value = null;
|
||||
spawn = BuildSpawn(pending.Source, pending.Amount, update);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void ClearSource(uint sourceGuid)
|
||||
{
|
||||
if (_value is { } pending && pending.Source.Guid == sourceGuid)
|
||||
_value = null;
|
||||
}
|
||||
|
||||
internal void Clear() => _value = null;
|
||||
|
||||
private static WorldSession.EntitySpawn BuildSpawn(
|
||||
WorldSession.EntitySpawn source,
|
||||
uint amount,
|
||||
WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
PhysicsSpawnData? physics = source.Physics is { } sourcePhysics
|
||||
? sourcePhysics with
|
||||
{
|
||||
Position = update.Position,
|
||||
Parent = null,
|
||||
Velocity = update.Velocity,
|
||||
Timestamps = sourcePhysics.Timestamps with
|
||||
{
|
||||
Position = update.PositionSequence,
|
||||
Teleport = update.TeleportSequence,
|
||||
ForcePosition = update.ForcePositionSequence,
|
||||
Instance = update.InstanceSequence,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
return source with
|
||||
{
|
||||
Guid = update.Guid,
|
||||
Position = update.Position,
|
||||
StackSize = checked((int)amount),
|
||||
ContainerId = 0u,
|
||||
WielderId = 0u,
|
||||
CurrentWieldedLocation = 0u,
|
||||
InstanceSequence = update.InstanceSequence,
|
||||
MovementSequence = 0,
|
||||
ServerControlSequence = 0,
|
||||
PositionSequence = update.PositionSequence,
|
||||
ParentGuid = null,
|
||||
ParentLocation = null,
|
||||
PlacementId = update.PlacementId,
|
||||
Physics = physics,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly record struct PendingSplit(
|
||||
ulong RequestToken,
|
||||
WorldSession.EntitySpawn Source,
|
||||
uint Amount,
|
||||
double RequestTime);
|
||||
}
|
||||
|
|
@ -676,6 +676,66 @@ AppearanceSynchronization:
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs the App projection for a canonical object whose accepted
|
||||
/// retail Position moved it from inventory-only ownership into the world.
|
||||
/// Logical identity, timestamps, setup defaults, and retained item state
|
||||
/// already exist; this crosses only the ordinary materialization/ready
|
||||
/// boundary.
|
||||
/// </summary>
|
||||
public bool RecoverCanonicalProjection(
|
||||
RuntimeEntityRecord expectedCanonical,
|
||||
ulong positionAuthorityVersion,
|
||||
out LiveEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
||||
lock (_datLock)
|
||||
{
|
||||
record = null!;
|
||||
if (!_runtime.IsCurrentPositionAuthority(
|
||||
expectedCanonical,
|
||||
positionAuthorityVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out record))
|
||||
{
|
||||
return _runtime.IsCurrentPositionAuthority(
|
||||
record,
|
||||
positionAuthorityVersion);
|
||||
}
|
||||
|
||||
WorldSession.EntitySpawn canonicalSpawn =
|
||||
expectedCanonical.Snapshot;
|
||||
if (canonicalSpawn.Position is null)
|
||||
return false;
|
||||
|
||||
InitializeOriginAndRecoverLoaded(canonicalSpawn);
|
||||
if (!_runtime.IsCurrentPositionAuthority(
|
||||
expectedCanonical,
|
||||
positionAuthorityVersion)
|
||||
|| !ProjectExact(
|
||||
expectedCanonical,
|
||||
canonicalSpawn,
|
||||
LiveProjectionPurpose.SpatialRecovery,
|
||||
expectedCanonical.CreateIntegrationVersion)
|
||||
|| !_runtime.TryGetProjection(
|
||||
expectedCanonical,
|
||||
out record))
|
||||
{
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
return _runtime.IsCurrentPositionAuthority(
|
||||
record,
|
||||
positionAuthorityVersion);
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
|
||||
{
|
||||
if (!PublishReady(candidate))
|
||||
|
|
|
|||
|
|
@ -1154,6 +1154,29 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a current top-level world projection even when its spatial
|
||||
/// bucket is pending/nonresident. Unlike interaction eligibility, this
|
||||
/// does not impose render visibility; unlike <see cref="TryGetWorldEntity"/>,
|
||||
/// it rejects a retained leave-world owner whose pose is historical.
|
||||
/// </summary>
|
||||
public bool TryGetSpatiallyProjectedRecord(
|
||||
uint serverGuid,
|
||||
out LiveEntityRecord record)
|
||||
{
|
||||
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord found)
|
||||
&& found.WorldEntity is not null
|
||||
&& found.ProjectionKind is LiveEntityProjectionKind.World
|
||||
&& found.IsSpatiallyProjected)
|
||||
{
|
||||
record = found;
|
||||
return true;
|
||||
}
|
||||
|
||||
record = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a top-level object that currently participates in picking,
|
||||
/// targeting, radar, and wire-driven MoveTo/Sticky establishment.
|
||||
|
|
@ -1762,6 +1785,12 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
&& ReferenceEquals(current, record)
|
||||
&& current.PositionAuthorityVersion == authorityVersion;
|
||||
|
||||
internal bool IsCurrentPositionAuthority(
|
||||
RuntimeEntityRecord canonical,
|
||||
ulong authorityVersion) =>
|
||||
_directory.IsCurrent(canonical)
|
||||
&& canonical.PositionAuthorityVersion == authorityVersion;
|
||||
|
||||
internal bool IsCurrentStateAuthority(
|
||||
LiveEntityRecord record,
|
||||
ulong authorityVersion) =>
|
||||
|
|
@ -1783,6 +1812,12 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
&& ReferenceEquals(current, record)
|
||||
&& current.VelocityAuthorityVersion == authorityVersion;
|
||||
|
||||
internal bool IsCurrentVelocityAuthority(
|
||||
RuntimeEntityRecord canonical,
|
||||
ulong authorityVersion) =>
|
||||
_directory.IsCurrent(canonical)
|
||||
&& canonical.VelocityAuthorityVersion == authorityVersion;
|
||||
|
||||
internal bool IsCurrentMovementAuthority(
|
||||
LiveEntityRecord record,
|
||||
ulong authorityVersion) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue