using AcDream.App.UI;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.App.World;
///
/// 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.
///
///
/// Retail records splitClassID, splitStackSize, and
/// splitTime in ACCWeenieObject::UIAttemptSplitTo3D @
/// 0x0058D850, then recognizes the created stack from
/// ACCWeenieObject::DeclareValid @ 0x0058E340. 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.
///
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 _now;
private bool _disposed;
public InventoryWorldDropProjectionController(
ItemInteractionController interaction,
ClientObjectTable objects,
LiveEntityRuntime runtime,
LiveEntityHydrationController hydration,
Func 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();
}
///
/// 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.
///
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,
// #314: every timestamp this record projects to the flattened
// spawn fields below MUST be reset here in the same breath.
// HasConsistentCreateIdentityAndParent
// (RuntimeEntityObjectLifetime) rejects a create whose
// PhysicsDesc timestamps disagree with their flattened
// projections, and the `source with { … }` below zeroes
// MovementSequence/ServerControlSequence. Omitting the
// matching Movement/ServerControlledMove resets here left a
// split whose SOURCE carried nonzero movement stamps — any
// item dropped once, picked back up, and split again —
// failing that predicate and throwing instead of completing
// the canonical create-placement transaction. A fresh split
// GUID has no movement history by construction, so zero is
// the honest value, not a placation of the predicate.
Timestamps = sourcePhysics.Timestamps with
{
Position = update.PositionSequence,
Teleport = update.TeleportSequence,
ForcePosition = update.ForcePositionSequence,
Instance = update.InstanceSequence,
Movement = 0,
ServerControlledMove = 0,
},
}
: 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);
}