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>
123 lines
3.4 KiB
C#
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();
|
|
}
|
|
}
|