fix(interaction): restore retail loot placement and world-drop projection

This commit is contained in:
Erik 2026-07-27 00:03:15 +02:00
parent 4d095be286
commit 921712f412
24 changed files with 1581 additions and 34 deletions

View file

@ -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,

View file

@ -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).