refactor(runtime): move accepted entity wire state

Move the presentation-free inbound physics timestamp/snapshot authority and parent-relation state into AcDream.Runtime.Entities without changing their control flow. Move their dedicated tests with them and keep App consumers as borrowers during the staged J3 cutover.

Validated by 26 focused Runtime tests, 232 focused App tests, the Release solution build, and 8,429 complete Release tests with five existing skips.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 19:52:29 +02:00
parent b0fecc5f65
commit f7442d13e9
18 changed files with 20 additions and 6 deletions

View file

@ -3,6 +3,7 @@ using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
namespace AcDream.App.Physics;

View file

@ -12,6 +12,7 @@ using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Items;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Core.Selection;
using AcDream.Core.World;
using DatReaderWriter;

View file

@ -12,6 +12,7 @@ using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using AcDream.Runtime.Entities;
namespace AcDream.App.Rendering;

View file

@ -1,692 +0,0 @@
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
namespace AcDream.App.World;
/// <summary>
/// Update-thread owner of inbound retail physics timestamps and the latest
/// accepted immutable spawn description. This is deliberately narrower than
/// <c>LiveEntityRuntime</c>: it owns no renderer, local entity ID, physics body,
/// animation, effect, or spatial-bucket resource.
/// </summary>
public sealed class InboundPhysicsStateController
{
private readonly Dictionary<uint, PhysicsTimestampGate> _gates = new();
private readonly Dictionary<uint, WorldSession.EntitySpawn> _snapshots = new();
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _snapshots;
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
_snapshots.TryGetValue(guid, out spawn);
public InboundCreateResult AcceptCreate(WorldSession.EntitySpawn incoming)
{
if (!_gates.TryGetValue(incoming.Guid, out PhysicsTimestampGate? gate))
{
gate = new PhysicsTimestampGate();
_gates.Add(incoming.Guid, gate);
}
PhysicsTimestamps timestamps = SpawnTimestamps(incoming);
CreateObjectTimestampDisposition disposition = gate.SeedForCreateObject(
timestamps.Position,
timestamps.Movement,
timestamps.State,
timestamps.Vector,
timestamps.Teleport,
timestamps.ServerControlledMove,
timestamps.ForcePosition,
timestamps.ObjDesc,
timestamps.Instance);
if (disposition is CreateObjectTimestampDisposition.StaleGeneration)
return new InboundCreateResult(disposition, default, null, Current(gate));
if (disposition is not CreateObjectTimestampDisposition.ExistingGeneration
|| !_snapshots.TryGetValue(incoming.Guid, out WorldSession.EntitySpawn retained))
{
_snapshots[incoming.Guid] = incoming;
return new InboundCreateResult(disposition, incoming, null, Current(gate));
}
WorldSession.EntitySpawn merged = MergeUntimestampedCreate(retained, incoming);
_snapshots[incoming.Guid] = merged;
return new InboundCreateResult(
disposition,
merged,
incoming.Physics is null ? null : BuildSameGenerationEvents(incoming),
Current(gate));
}
public bool TryDelete(DeleteObject.Parsed delete, bool isLocalPlayer)
{
if (!_gates.TryGetValue(delete.Guid, out PhysicsTimestampGate? gate))
return false;
if (!gate.TryAcceptDeleteEvent(delete.InstanceSequence, isLocalPlayer))
return false;
_gates.Remove(delete.Guid);
_snapshots.Remove(delete.Guid);
return true;
}
public bool TryApplyObjDesc(
ObjDescEvent.Parsed update,
out WorldSession.EntitySpawn accepted)
{
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|| !gate.TryAcceptObjDescEvent(update.InstanceSequence, update.ObjDescSequence))
{
accepted = default;
return false;
}
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
{
Timestamps = desc.Timestamps with { ObjDesc = update.ObjDescSequence },
};
accepted = old with
{
AnimPartChanges = update.ModelData.AnimPartChanges,
TextureChanges = update.ModelData.TextureChanges,
SubPalettes = update.ModelData.SubPalettes,
BasePaletteId = update.ModelData.BasePaletteId,
Physics = physics,
};
_snapshots[update.Guid] = accepted;
return true;
}
public bool TryApplyPickup(
PickupEvent.Parsed update,
out WorldSession.EntitySpawn accepted)
{
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|| !gate.TryAcceptPositionChannelEvent(
update.InstanceSequence, update.PositionSequence))
{
accepted = default;
return false;
}
accepted = ApplyUnparentedPosition(old, null, update.PositionSequence);
_snapshots[update.Guid] = accepted;
return true;
}
/// <summary>
/// Applies the parent branch embedded in a same-generation PhysicsDesc.
/// Unlike standalone ParentEvent it carries no parent INSTANCE_TS, so only
/// the child's shared POSITION_TS participates in freshness.
/// </summary>
public bool TryApplyCreateParent(
CreateParentUpdate update,
out WorldSession.EntitySpawn accepted)
{
if (!TryGet(update.ChildGuid, out PhysicsTimestampGate? childGate, out WorldSession.EntitySpawn child)
|| !childGate.TryAcceptPositionChannelEvent(
update.ChildInstanceSequence, update.ChildPositionSequence))
{
accepted = default;
return false;
}
accepted = ApplyPositionTimestampOnly(child, update.ChildPositionSequence);
_snapshots[update.ChildGuid] = accepted;
return true;
}
public bool TryApplyParent(
ParentEvent.Parsed update,
out WorldSession.EntitySpawn accepted)
{
if (!_gates.TryGetValue(update.ParentGuid, out PhysicsTimestampGate? parentGate)
|| !parentGate.IsCurrentInstance(update.ParentInstanceSequence)
|| !TryGet(update.ChildGuid, out PhysicsTimestampGate? childGate, out WorldSession.EntitySpawn child)
|| !childGate.TryAcceptPositionChannelEvent(
childGate.InstanceTimestamp, update.ChildPositionSequence))
{
accepted = default;
return false;
}
accepted = ApplyPositionTimestampOnly(child, update.ChildPositionSequence);
_snapshots[update.ChildGuid] = accepted;
return true;
}
public bool TryCommitParent(
uint childGuid,
uint parentGuid,
uint parentLocation,
uint placementId,
ushort positionSequence,
out WorldSession.EntitySpawn accepted)
{
if (!TryGet(childGuid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn child)
|| gate.PositionTimestamp != positionSequence
|| child.PositionSequence != positionSequence)
{
accepted = default;
return false;
}
accepted = ApplyParent(
child,
parentGuid,
parentLocation,
placementId,
positionSequence);
_snapshots[childGuid] = accepted;
return true;
}
public bool TryApplyMotion(
WorldSession.EntityMotionUpdate update,
bool retainPayload,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps)
{
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old))
{
accepted = default;
timestamps = default;
return false;
}
bool applyPayload = gate.TryAcceptMovementEvent(
update.InstanceSequence,
update.MovementSequence,
update.ServerControlSequence);
timestamps = Current(gate);
WorldSession.EntitySpawn stamped = MirrorGateTimestamps(old, gate) with
{
MovementSequence = gate.MovementTimestamp,
ServerControlSequence = gate.ServerControlledMoveTimestamp,
};
_snapshots[update.Guid] = stamped;
// Retail consumes MOVEMENT_TS before it discovers that the
// SERVER_CONTROLLED_MOVE_TS is stale. Preserve that timestamp-only
// mutation in the canonical snapshot even though no motion payload
// is applied.
if (!applyPayload)
{
accepted = default;
return false;
}
if (!retainPayload)
{
accepted = stamped;
return true;
}
PhysicsSpawnData? physics = stamped.Physics;
if (physics is { } desc)
physics = desc with
{
Movement = new PhysicsMovementData(
ReadOnlyMemory<byte>.Empty,
update.MotionState,
update.IsAutonomous),
Timestamps = desc.Timestamps with
{
Movement = gate.MovementTimestamp,
ServerControlledMove = gate.ServerControlledMoveTimestamp,
},
};
accepted = stamped with
{
MotionState = update.MotionState,
Physics = physics,
};
_snapshots[update.Guid] = accepted;
return true;
}
public bool TryApplyVector(
VectorUpdate.Parsed update,
out WorldSession.EntitySpawn accepted)
{
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|| !gate.TryAcceptVectorEvent(update.InstanceSequence, update.VectorSequence))
{
accepted = default;
return false;
}
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
{
Velocity = update.Velocity,
AngularVelocity = update.Omega,
Timestamps = desc.Timestamps with { Vector = update.VectorSequence },
};
accepted = old with { Physics = physics };
_snapshots[update.Guid] = accepted;
return true;
}
public bool TryApplyState(
SetState.Parsed update,
out WorldSession.EntitySpawn accepted)
{
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old)
|| !gate.TryAcceptStateEvent(update.InstanceSequence, update.StateSequence))
{
accepted = default;
return false;
}
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
{
RawState = update.PhysicsState,
Timestamps = desc.Timestamps with { State = update.StateSequence },
};
accepted = old with
{
PhysicsState = update.PhysicsState,
Physics = physics,
};
_snapshots[update.Guid] = accepted;
return true;
}
/// <summary>
/// Returns true when the addressed live incarnation exists, even when the
/// position payload is rejected. This lets callers publish a freshly
/// consumed FORCE_POSITION_TS without applying a stale pose.
/// </summary>
public bool TryApplyPosition(
WorldSession.EntityPositionUpdate update,
bool isLocalPlayer,
System.Numerics.Quaternion? forcePositionRotation,
System.Numerics.Vector3? currentLocalVelocity,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps)
{
if (!TryGet(update.Guid, out PhysicsTimestampGate? gate, out WorldSession.EntitySpawn old))
{
disposition = PositionTimestampDisposition.Rejected;
accepted = default;
timestamps = default;
return false;
}
bool advancesTeleport = PhysicsTimestampGate.IsNewer(
gate.TeleportTimestamp,
update.TeleportSequence);
disposition = gate.TryAcceptPositionEvent(
update.InstanceSequence,
update.PositionSequence,
update.TeleportSequence,
update.ForcePositionSequence,
isLocalPlayer);
timestamps = Current(
gate,
teleportAdvanced: disposition is PositionTimestampDisposition.Apply
&& advancesTeleport);
if (disposition is PositionTimestampDisposition.Rejected)
{
accepted = MirrorGateTimestamps(old, gate);
_snapshots[update.Guid] = accepted;
return true;
}
CreateObject.ServerPosition appliedPosition = update.Position;
if (disposition is PositionTimestampDisposition.ForcePosition
&& forcePositionRotation is { } preserved)
{
appliedPosition = update.Position with
{
RotationW = preserved.W,
RotationX = preserved.X,
RotationY = preserved.Y,
RotationZ = preserved.Z,
};
}
// PositionPack::UnPack (0x00516740) initializes an absent placement
// id to zero; HandleReceivedPosition (0x00453FD0) forwards that exact
// value to SetPlacementFrame on a normal accepted update.
uint? appliedPlacement = disposition is PositionTimestampDisposition.Apply
? update.PlacementId ?? 0u
: old.PlacementId;
System.Numerics.Vector3? appliedVelocity = disposition switch
{
// ForcePosition returns immediately after BlipPlayer and retains
// the local physics object's velocity.
PositionTimestampDisposition.ForcePosition =>
currentLocalVelocity ?? old.Physics?.Velocity,
// A fresh local teleport explicitly installs zero velocity. A
// normal local correction does not consume PositionPack velocity.
PositionTimestampDisposition.Apply when isLocalPlayer =>
advancesTeleport
? System.Numerics.Vector3.Zero
: currentLocalVelocity ?? old.Physics?.Velocity,
// Remote MoveOrTeleport receives the unpacked vector; an absent
// PositionPack velocity is initialized to zero by retail.
PositionTimestampDisposition.Apply =>
update.Velocity ?? System.Numerics.Vector3.Zero,
_ => old.Physics?.Velocity,
};
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
{
Position = appliedPosition,
AnimationFrame = appliedPlacement,
Velocity = appliedVelocity,
Parent = null,
Timestamps = desc.Timestamps with
{
Position = gate.PositionTimestamp,
Teleport = gate.TeleportTimestamp,
ForcePosition = gate.ForcePositionTimestamp,
},
};
accepted = old with
{
Position = appliedPosition,
PositionSequence = gate.PositionTimestamp,
ParentGuid = null,
ParentLocation = null,
PlacementId = appliedPlacement,
Physics = physics,
};
_snapshots[update.Guid] = accepted;
return true;
}
/// <summary>
/// F751 is a notification gate only. Retail compares it to TELEPORT_TS but
/// advances that timestamp later, with the accepted Position packet.
/// </summary>
public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) =>
_gates.TryGetValue(localPlayerGuid, out PhysicsTimestampGate? gate)
&& gate.IsFreshTeleportStart(teleportSequence);
public void Clear()
{
_gates.Clear();
_snapshots.Clear();
}
private bool TryGet(
uint guid,
out PhysicsTimestampGate gate,
out WorldSession.EntitySpawn spawn)
{
if (_gates.TryGetValue(guid, out gate!)
&& _snapshots.TryGetValue(guid, out spawn))
return true;
gate = null!;
spawn = default;
return false;
}
private static PhysicsTimestamps SpawnTimestamps(WorldSession.EntitySpawn spawn) =>
spawn.Physics?.Timestamps
?? new PhysicsTimestamps(
spawn.PositionSequence,
spawn.MovementSequence,
0,
0,
0,
spawn.ServerControlSequence,
0,
0,
spawn.InstanceSequence);
private static AcceptedPhysicsTimestamps Current(
PhysicsTimestampGate gate,
bool teleportAdvanced = false) => new(
gate.InstanceTimestamp,
gate.ServerControlledMoveTimestamp,
gate.TeleportTimestamp,
gate.ForcePositionTimestamp,
teleportAdvanced);
private static WorldSession.EntitySpawn MirrorGateTimestamps(
WorldSession.EntitySpawn spawn,
PhysicsTimestampGate gate)
{
if (spawn.Physics is not { } desc)
return spawn;
return spawn with
{
Physics = desc with
{
Timestamps = new PhysicsTimestamps(
gate.PositionTimestamp,
gate.MovementTimestamp,
gate.StateTimestamp,
gate.VectorTimestamp,
gate.TeleportTimestamp,
gate.ServerControlledMoveTimestamp,
gate.ForcePositionTimestamp,
gate.ObjDescTimestamp,
gate.InstanceTimestamp),
},
};
}
private static WorldSession.EntitySpawn MergeUntimestampedCreate(
WorldSession.EntitySpawn retained,
WorldSession.EntitySpawn incoming) =>
incoming with
{
Position = retained.Position,
SetupTableId = retained.SetupTableId,
AnimPartChanges = retained.AnimPartChanges,
TextureChanges = retained.TextureChanges,
SubPalettes = retained.SubPalettes,
BasePaletteId = retained.BasePaletteId,
ObjScale = retained.ObjScale,
MotionState = retained.MotionState,
MotionTableId = retained.MotionTableId,
PhysicsState = retained.PhysicsState,
Friction = retained.Friction,
Elasticity = retained.Elasticity,
InstanceSequence = retained.InstanceSequence,
MovementSequence = retained.MovementSequence,
ServerControlSequence = retained.ServerControlSequence,
PositionSequence = retained.PositionSequence,
ParentGuid = retained.ParentGuid,
ParentLocation = retained.ParentLocation,
PlacementId = retained.PlacementId,
Physics = retained.Physics,
};
private static SameGenerationCreateObjectEvents BuildSameGenerationEvents(
WorldSession.EntitySpawn incoming)
{
PhysicsSpawnData physics = incoming.Physics!.Value;
PhysicsTimestamps ts = physics.Timestamps;
CreateParentUpdate? parent = physics.Parent is { } attachment
? new CreateParentUpdate(
incoming.Guid,
attachment.Guid,
attachment.LocationId,
physics.AnimationFrame ?? 0u,
ts.Instance,
ts.Position)
: null;
WorldSession.EntityPositionUpdate? position = parent is null
&& physics.Position is { } p
&& p.LandblockId != 0
? new WorldSession.EntityPositionUpdate(
incoming.Guid,
p,
physics.Velocity,
physics.AnimationFrame,
IsGrounded: true,
ts.Instance,
ts.Position,
ts.Teleport,
ts.ForcePosition)
: null;
PickupEvent.Parsed? pickup = parent is null && position is null
? new PickupEvent.Parsed(incoming.Guid, ts.Instance, ts.Position)
: null;
WorldSession.EntityMotionUpdate? movement =
physics.Movement is { } movementData
&& !movementData.RawData.IsEmpty
&& movementData.MotionState is { } motionState
? new WorldSession.EntityMotionUpdate(
incoming.Guid,
motionState,
ts.Instance,
ts.Movement,
ts.ServerControlledMove,
movementData.IsAutonomous is true)
: null;
return new SameGenerationCreateObjectEvents(
physics,
new ObjDescEvent.Parsed(
incoming.Guid,
new CreateObject.ModelData(
incoming.BasePaletteId,
incoming.SubPalettes,
incoming.TextureChanges,
incoming.AnimPartChanges),
ts.Instance,
ts.ObjDesc),
parent,
position,
pickup,
movement,
new SetState.Parsed(incoming.Guid, physics.RawState, ts.Instance, ts.State),
new VectorUpdate.Parsed(
incoming.Guid,
physics.Velocity ?? System.Numerics.Vector3.Zero,
physics.AngularVelocity ?? System.Numerics.Vector3.Zero,
ts.Instance,
ts.Vector));
}
private static WorldSession.EntitySpawn ApplyUnparentedPosition(
WorldSession.EntitySpawn old,
CreateObject.ServerPosition? position,
ushort positionSequence)
{
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
{
Position = position,
Parent = null,
Timestamps = desc.Timestamps with { Position = positionSequence },
};
return old with
{
Position = position,
ParentGuid = null,
ParentLocation = null,
PositionSequence = positionSequence,
Physics = physics,
};
}
private static WorldSession.EntitySpawn ApplyPositionTimestampOnly(
WorldSession.EntitySpawn old,
ushort positionSequence)
{
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
{
physics = desc with
{
Timestamps = desc.Timestamps with { Position = positionSequence },
};
}
return old with
{
PositionSequence = positionSequence,
Physics = physics,
};
}
private static WorldSession.EntitySpawn ApplyParent(
WorldSession.EntitySpawn old,
uint parentGuid,
uint parentLocation,
uint placementId,
ushort positionSequence)
{
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
{
Position = null,
Parent = new PhysicsAttachment(parentGuid, parentLocation),
AnimationFrame = placementId,
Timestamps = desc.Timestamps with { Position = positionSequence },
};
return old with
{
Position = null,
ParentGuid = parentGuid,
ParentLocation = parentLocation,
PlacementId = placementId,
PositionSequence = positionSequence,
Physics = physics,
};
}
}
public readonly record struct AcceptedPhysicsTimestamps(
ushort Instance,
ushort ServerControlledMove,
ushort Teleport,
ushort ForcePosition,
bool TeleportAdvanced = false,
bool TeleportHookRequired = false);
public readonly record struct CreateParentUpdate(
uint ChildGuid,
uint ParentGuid,
uint ParentLocation,
uint PlacementId,
ushort ChildInstanceSequence,
ushort ChildPositionSequence);
public readonly record struct SameGenerationCreateObjectEvents(
PhysicsSpawnData Description,
ObjDescEvent.Parsed Appearance,
CreateParentUpdate? Parent,
WorldSession.EntityPositionUpdate? Position,
PickupEvent.Parsed? Pickup,
WorldSession.EntityMotionUpdate? Movement,
SetState.Parsed State,
VectorUpdate.Parsed Vector);
public readonly record struct InboundCreateResult(
CreateObjectTimestampDisposition Disposition,
WorldSession.EntitySpawn Snapshot,
SameGenerationCreateObjectEvents? SameGenerationEvents,
AcceptedPhysicsTimestamps Timestamps);

View file

@ -4,6 +4,7 @@ using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
namespace AcDream.App.World;

View file

@ -5,6 +5,7 @@ using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Runtime.Entities;
namespace AcDream.App.World;

View file

@ -3,6 +3,7 @@ using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
using System.Numerics;
using System.Runtime.ExceptionServices;

View file

@ -1,5 +1,6 @@
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Runtime.Entities;
using AcDream.Core.Physics;
namespace AcDream.App.World;

View file

@ -1,5 +1,6 @@
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.Runtime.Entities;
namespace AcDream.App.World;

View file

@ -1,396 +0,0 @@
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
namespace AcDream.App.World;
/// <summary>
/// Update-thread state machine for parent relations that may arrive before
/// either CreateObject. Unaccepted wire events remain in arrival order;
/// committed rollback history contains only relations that passed both the
/// canonical retail timestamp gate and retail <c>PartArray::add_child</c>
/// validation.
/// </summary>
public sealed class ParentAttachmentState
{
private readonly Dictionary<uint, Queue<ParentAttachmentRelation>> _unresolvedByChild = new();
private readonly Dictionary<uint, ParentAttachmentRelation> _stagedByChild = new();
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
{
_stagedByChild[relation.ChildGuid] = relation;
}
public void Enqueue(ParentEvent.Parsed update)
{
if (!_unresolvedByChild.TryGetValue(update.ChildGuid, out Queue<ParentAttachmentRelation>? queue))
{
queue = new Queue<ParentAttachmentRelation>();
_unresolvedByChild.Add(update.ChildGuid, queue);
}
queue.Enqueue(new ParentAttachmentRelation(
update.ParentGuid,
update.ChildGuid,
update.ParentLocation,
update.PlacementId,
update.ParentInstanceSequence,
update.ChildPositionSequence));
}
/// <summary>
/// Resolves every now-addressable relation for one child in wire order.
/// A future parent generation remains queued; an older generation or a
/// rejected child POSITION_TS is discarded. Every accepted relation
/// supersedes the preceding render projection.
/// </summary>
public void Resolve(
uint childGuid,
Func<uint, bool> isObjectKnown,
Func<uint, ushort?> resolveInstance,
Func<ParentEvent.Parsed, bool> accept)
{
if (_stagedByChild.ContainsKey(childGuid))
return;
if (!_unresolvedByChild.TryGetValue(childGuid, out Queue<ParentAttachmentRelation>? queue))
return;
int candidateCount = queue.Count;
for (int i = 0; i < candidateCount; i++)
{
ParentAttachmentRelation relation = queue.Dequeue();
ushort? parentInstance = resolveInstance(relation.ParentGuid);
if (parentInstance is null)
{
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
continue;
}
if (parentInstance.Value != relation.ParentInstanceSequence)
{
// Current parent newer than the packet: discard the stale
// relation. Packet newer than current: retain it until that
// parent generation is constructed.
if (PhysicsTimestampGate.IsNewer(
relation.ParentInstanceSequence,
parentInstance.Value))
{
continue;
}
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
continue;
}
if (!isObjectKnown(childGuid))
{
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Child });
continue;
}
var update = new ParentEvent.Parsed(
relation.ParentGuid,
relation.ChildGuid,
relation.ParentLocation,
relation.PlacementId,
relation.ParentInstanceSequence,
relation.ChildPositionSequence);
if (!accept(update))
continue;
_stagedByChild[childGuid] = relation with
{
WaitOwner = ParentAttachmentWaitOwner.Unknown,
};
break;
}
if (queue.Count == 0)
_unresolvedByChild.Remove(childGuid);
}
public bool TryGetProjection(uint childGuid, out ParentAttachmentRelation relation) =>
_stagedByChild.TryGetValue(childGuid, out relation)
|| _recoveryByChild.TryGetValue(childGuid, out relation);
public bool TryGetStagedProjection(
uint childGuid,
out ParentAttachmentRelation relation) =>
_stagedByChild.TryGetValue(childGuid, out relation);
public bool TryGetRecoveryProjection(
uint childGuid,
out ParentAttachmentRelation relation) =>
_recoveryByChild.TryGetValue(childGuid, out relation);
public void CopyPendingProjectionChildrenTo(List<uint> destination)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach (uint childGuid in _stagedByChild.Keys)
destination.Add(childGuid);
foreach (uint childGuid in _recoveryByChild.Keys)
{
if (!_stagedByChild.ContainsKey(childGuid))
destination.Add(childGuid);
}
}
public bool IsCommitted(ParentAttachmentRelation relation) =>
_lastAcceptedByChild.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation committed)
&& committed == relation;
public bool IsPending(
ParentAttachmentRelation relation,
ParentProjectionCandidateKind kind) =>
(kind is ParentProjectionCandidateKind.Staged
? _stagedByChild
: _recoveryByChild).TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation pending)
&& pending == relation;
public void MarkProjected(
ParentAttachmentRelation relation,
ParentProjectionCandidateKind kind)
{
Dictionary<uint, ParentAttachmentRelation> source =
kind is ParentProjectionCandidateKind.Staged
? _stagedByChild
: _recoveryByChild;
if (source.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation pending)
&& pending == relation)
{
source.Remove(relation.ChildGuid);
}
}
public bool CommitProjection(ParentAttachmentRelation relation)
{
if (!_stagedByChild.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation staged)
|| staged != relation)
{
return false;
}
_lastAcceptedByChild[relation.ChildGuid] = relation;
_stagedByChild.Remove(relation.ChildGuid);
_recoveryByChild[relation.ChildGuid] = relation;
return true;
}
public void RejectProjection(ParentAttachmentRelation relation)
{
if (_stagedByChild.TryGetValue(
relation.ChildGuid,
out ParentAttachmentRelation staged)
&& staged == relation)
{
_stagedByChild.Remove(relation.ChildGuid);
}
}
public bool RestoreLastAccepted(uint childGuid)
{
if (!_lastAcceptedByChild.TryGetValue(childGuid, out ParentAttachmentRelation relation))
return false;
_recoveryByChild[childGuid] = relation;
return true;
}
public IReadOnlyList<uint> ChildrenWaitingForParent(uint parentGuid)
{
var result = new HashSet<uint>();
foreach ((uint childGuid, ParentAttachmentRelation relation) in _stagedByChild)
{
if (relation.ParentGuid == parentGuid)
result.Add(childGuid);
}
foreach ((uint childGuid, ParentAttachmentRelation relation) in _recoveryByChild)
{
if (relation.ParentGuid == parentGuid)
result.Add(childGuid);
}
foreach ((uint childGuid, Queue<ParentAttachmentRelation> queue) in _unresolvedByChild)
{
if (queue.Any(relation => relation.ParentGuid == parentGuid))
result.Add(childGuid);
}
return result.ToArray();
}
public void RemoveObject(uint guid)
{
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
_lastAcceptedByChild.Remove(guid);
_unresolvedByChild.Remove(guid);
RemoveParentReferences(_stagedByChild, guid);
RemoveParentReferences(_recoveryByChild, guid);
RemoveParentReferences(_lastAcceptedByChild, guid);
uint[] children = _unresolvedByChild.Keys.ToArray();
for (int i = 0; i < children.Length; i++)
{
uint childGuid = children[i];
Queue<ParentAttachmentRelation> retained = new(
_unresolvedByChild[childGuid].Where(relation => relation.ParentGuid != guid));
if (retained.Count == 0)
_unresolvedByChild.Remove(childGuid);
else
_unresolvedByChild[childGuid] = retained;
}
}
/// <summary>
/// Ends one logical incarnation without discarding unresolved wire events
/// that may explicitly address the replacement/future parent generation.
/// </summary>
public void EndGeneration(uint guid, ushort replacementGeneration)
{
FilterChildCandidates(
guid,
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
_lastAcceptedByChild.Remove(guid);
RemoveParentReferences(_stagedByChild, guid);
RemoveParentReferences(_recoveryByChild, guid);
RemoveParentReferences(_lastAcceptedByChild, guid);
FilterParentCandidates(
guid,
relation => relation.ParentInstanceSequence == replacementGeneration
|| PhysicsTimestampGate.IsNewer(
replacementGeneration,
relation.ParentInstanceSequence));
}
/// <summary>
/// Deletes one exact incarnation. Child-addressed candidates die with the
/// child; candidates addressed to a strictly newer parent incarnation
/// survive for retail's post-Create blob replay.
/// </summary>
public void DeleteGeneration(uint guid, ushort deletedGeneration)
{
FilterChildCandidates(
guid,
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
_lastAcceptedByChild.Remove(guid);
RemoveParentReferences(_stagedByChild, guid);
RemoveParentReferences(_recoveryByChild, guid);
RemoveParentReferences(_lastAcceptedByChild, guid);
FilterParentCandidates(
guid,
relation => PhysicsTimestampGate.IsNewer(
deletedGeneration,
relation.ParentInstanceSequence));
}
/// <summary>
/// Clears an accepted attachment projection after Pickup or a world
/// Position without destroying fresher unresolved ParentEvents.
/// </summary>
public void EndChildProjection(uint childGuid)
{
_stagedByChild.Remove(childGuid);
_recoveryByChild.Remove(childGuid);
_lastAcceptedByChild.Remove(childGuid);
}
public void RemoveChild(uint childGuid)
{
_stagedByChild.Remove(childGuid);
_recoveryByChild.Remove(childGuid);
_lastAcceptedByChild.Remove(childGuid);
_unresolvedByChild.Remove(childGuid);
}
public void Clear()
{
_unresolvedByChild.Clear();
_stagedByChild.Clear();
_recoveryByChild.Clear();
_lastAcceptedByChild.Clear();
}
private static void RemoveParentReferences(
Dictionary<uint, ParentAttachmentRelation> relations,
uint parentGuid)
{
uint[] children = relations
.Where(pair => pair.Value.ParentGuid == parentGuid)
.Select(pair => pair.Key)
.ToArray();
for (int i = 0; i < children.Length; i++)
relations.Remove(children[i]);
}
private void FilterParentCandidates(
uint parentGuid,
Func<ParentAttachmentRelation, bool> retain)
{
uint[] children = _unresolvedByChild.Keys.ToArray();
for (int i = 0; i < children.Length; i++)
{
uint childGuid = children[i];
Queue<ParentAttachmentRelation> queue = _unresolvedByChild[childGuid];
var retained = new Queue<ParentAttachmentRelation>(
queue.Where(relation => relation.ParentGuid != parentGuid || retain(relation)));
if (retained.Count == 0)
_unresolvedByChild.Remove(childGuid);
else
_unresolvedByChild[childGuid] = retained;
}
}
private void FilterChildCandidates(
uint childGuid,
Func<ParentAttachmentRelation, bool> retain)
{
if (!_unresolvedByChild.TryGetValue(
childGuid,
out Queue<ParentAttachmentRelation>? queue))
{
return;
}
var retained = new Queue<ParentAttachmentRelation>(queue.Where(retain));
if (retained.Count == 0)
_unresolvedByChild.Remove(childGuid);
else
_unresolvedByChild[childGuid] = retained;
}
}
public readonly record struct ParentAttachmentRelation(
uint ParentGuid,
uint ChildGuid,
uint ParentLocation,
uint PlacementId,
ushort ParentInstanceSequence,
ushort ChildPositionSequence,
ParentAttachmentWaitOwner WaitOwner = ParentAttachmentWaitOwner.Unknown);
public enum ParentAttachmentWaitOwner
{
Unknown,
Parent,
Child,
}
public enum ParentProjectionCandidateKind
{
Staged,
Recovery,
}