acdream/src/AcDream.App/World/RetailInboundEventDispatcher.cs
Erik f961d70023 feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-20 09:10:31 +02:00

123 lines
3.4 KiB
C#

namespace AcDream.App.World;
/// <summary>
/// Serializes retail network/event mutations on the update thread.
/// </summary>
/// <remarks>
/// Retail SmartBox dispatch and CPhysics::UseTime never interleave two packet
/// bodies on one CPhysicsObj call stack. App callbacks are synchronous, so an
/// observer can otherwise re-enter a session event while an older packet or
/// object quantum is still unwinding. Nested work is retained in arrival
/// order and drained only after the current operation reaches its complete
/// retail tail. This preserves the protocol's independent timestamp channels:
/// an accepted State never cancels an accepted Position merely because their
/// callbacks touched the same object.
/// </remarks>
internal sealed class RetailInboundEventDispatcher
{
private interface IPendingOperation
{
void Invoke();
}
private sealed class PendingAction(Action operation) : IPendingOperation
{
public void Invoke() => operation();
}
private sealed class PendingStateOperation<TReceiver, TState>(
TReceiver receiver,
TState state,
Action<TReceiver, TState> operation) : IPendingOperation
{
public void Invoke() => operation(receiver, state);
}
private readonly Queue<IPendingOperation> _pending = new();
private bool _isDraining;
internal int PendingCount => _pending.Count;
internal bool IsDraining => _isDraining;
internal void Run(Action operation)
{
ArgumentNullException.ThrowIfNull(operation);
if (_isDraining)
{
_pending.Enqueue(new PendingAction(operation));
return;
}
_isDraining = true;
try
{
operation();
DrainPending();
}
catch
{
// A failed packet cannot leave later packets stranded for an
// unrelated future frame, or replay them after partial teardown.
_pending.Clear();
throw;
}
finally
{
_isDraining = false;
}
}
/// <summary>
/// Allocation-free non-reentrant dispatch for hot frame and packet paths.
/// A heterogeneous wrapper is created only when an operation genuinely
/// re-enters the active retail FIFO and therefore must outlive this call.
/// </summary>
internal void Run<TReceiver, TState>(
TReceiver receiver,
TState state,
Action<TReceiver, TState> operation)
{
ArgumentNullException.ThrowIfNull(operation);
if (_isDraining)
{
_pending.Enqueue(
new PendingStateOperation<TReceiver, TState>(
receiver,
state,
operation));
return;
}
_isDraining = true;
try
{
operation(receiver, state);
DrainPending();
}
catch
{
_pending.Clear();
throw;
}
finally
{
_isDraining = false;
}
}
private void DrainPending()
{
while (_pending.TryDequeue(out IPendingOperation? next))
next.Invoke();
}
internal void Clear()
{
if (_isDraining)
{
throw new InvalidOperationException(
"Inbound event work cannot be cleared while its retail FIFO is active.");
}
_pending.Clear();
}
}