Publish the retail blocking-for-cells edge before deferred recenter work, freeze old-world presentation/simulation/audio, and advance full-window retirement from exact metered entity and owner cursors. This removes synchronous portal teardown without allowing retained owners to remain observable.
743 lines
25 KiB
C#
743 lines
25 KiB
C#
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
[Flags]
|
|
public enum LandblockRetirementStage : ushort
|
|
{
|
|
None = 0,
|
|
MeshReferences = 1 << 0,
|
|
StaticScripts = 1 << 1,
|
|
Classification = 1 << 2,
|
|
EntityLighting = 1 << 3,
|
|
EntityTranslucency = 1 << 4,
|
|
PluginProjection = 1 << 5,
|
|
Terrain = 1 << 6,
|
|
Physics = 1 << 7,
|
|
CellVisibility = 1 << 8,
|
|
BuildingRegistry = 1 << 9,
|
|
EnvironmentCells = 1 << 10,
|
|
LegacyPresentation = 1 << 11,
|
|
}
|
|
|
|
internal enum LandblockRetirementOperationResult : byte
|
|
{
|
|
NoWork,
|
|
Progressed,
|
|
Failed,
|
|
}
|
|
|
|
/// <summary>
|
|
/// One retryable landblock-retirement ledger. Successful owner operations are
|
|
/// committed once; a later attempt resumes at the exact failed owner/entity.
|
|
/// </summary>
|
|
public sealed class LandblockRetirementTicket
|
|
{
|
|
private readonly Dictionary<LandblockRetirementStage, int> _entityCursors = new();
|
|
private readonly Dictionary<LandblockRetirementStage, Exception> _failures = new();
|
|
private Exception? _callbackFailure;
|
|
private Exception? _lastReportedFailure;
|
|
|
|
internal LandblockRetirementTicket(
|
|
GpuLandblockRetirement state,
|
|
LandblockRetirementStage requiredStages)
|
|
{
|
|
State = state;
|
|
RequiredStages = requiredStages;
|
|
}
|
|
|
|
public GpuLandblockRetirement State { get; }
|
|
public uint LandblockId => State.LandblockId;
|
|
public LandblockRetirementKind Kind => State.Kind;
|
|
public IReadOnlyList<WorldEntity> Entities => State.Entities;
|
|
public LandblockRetirementStage RequiredStages { get; }
|
|
public LandblockRetirementStage CompletedStages { get; private set; }
|
|
public bool IsComplete =>
|
|
(CompletedStages & RequiredStages) == RequiredStages
|
|
&& _callbackFailure is null;
|
|
|
|
public bool RunOnce(LandblockRetirementStage stage, Action operation)
|
|
{
|
|
ValidateSingleStage(stage);
|
|
ArgumentNullException.ThrowIfNull(operation);
|
|
if ((CompletedStages & stage) != 0)
|
|
return true;
|
|
|
|
try
|
|
{
|
|
operation();
|
|
CompletedStages |= stage;
|
|
_failures.Remove(stage);
|
|
return true;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
_failures[stage] = error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal LandblockRetirementOperationResult RunOnceStep(
|
|
LandblockRetirementStage stage,
|
|
Action operation)
|
|
{
|
|
ValidateSingleStage(stage);
|
|
ArgumentNullException.ThrowIfNull(operation);
|
|
if ((CompletedStages & stage) != 0)
|
|
return LandblockRetirementOperationResult.NoWork;
|
|
|
|
try
|
|
{
|
|
operation();
|
|
CompletedStages |= stage;
|
|
_failures.Remove(stage);
|
|
return LandblockRetirementOperationResult.Progressed;
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
_failures[stage] = error;
|
|
return LandblockRetirementOperationResult.Failed;
|
|
}
|
|
}
|
|
|
|
public bool RunForEachEntity(
|
|
LandblockRetirementStage stage,
|
|
Func<WorldEntity, bool> predicate,
|
|
Action<WorldEntity> operation)
|
|
{
|
|
ValidateSingleStage(stage);
|
|
ArgumentNullException.ThrowIfNull(predicate);
|
|
ArgumentNullException.ThrowIfNull(operation);
|
|
if ((CompletedStages & stage) != 0)
|
|
return true;
|
|
|
|
_entityCursors.TryGetValue(stage, out int cursor);
|
|
while (cursor < Entities.Count)
|
|
{
|
|
WorldEntity entity = Entities[cursor];
|
|
if (predicate(entity))
|
|
{
|
|
try
|
|
{
|
|
operation(entity);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
_entityCursors[stage] = cursor;
|
|
_failures[stage] = error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
cursor++;
|
|
_entityCursors[stage] = cursor;
|
|
}
|
|
|
|
CompletedStages |= stage;
|
|
_entityCursors.Remove(stage);
|
|
_failures.Remove(stage);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances exactly one entity cursor, including predicate misses. This
|
|
/// makes both owner work and the scan needed to find it visible to the
|
|
/// frame budget.
|
|
/// </summary>
|
|
internal LandblockRetirementOperationResult RunEntityStep(
|
|
LandblockRetirementStage stage,
|
|
Func<WorldEntity, bool> predicate,
|
|
Action<WorldEntity> operation)
|
|
{
|
|
ValidateSingleStage(stage);
|
|
ArgumentNullException.ThrowIfNull(predicate);
|
|
ArgumentNullException.ThrowIfNull(operation);
|
|
if ((CompletedStages & stage) != 0)
|
|
return LandblockRetirementOperationResult.NoWork;
|
|
|
|
_entityCursors.TryGetValue(stage, out int cursor);
|
|
if (cursor >= Entities.Count)
|
|
{
|
|
CompletedStages |= stage;
|
|
_entityCursors.Remove(stage);
|
|
_failures.Remove(stage);
|
|
return LandblockRetirementOperationResult.Progressed;
|
|
}
|
|
|
|
WorldEntity entity = Entities[cursor];
|
|
if (predicate(entity))
|
|
{
|
|
try
|
|
{
|
|
operation(entity);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
_entityCursors[stage] = cursor;
|
|
_failures[stage] = error;
|
|
return LandblockRetirementOperationResult.Failed;
|
|
}
|
|
}
|
|
|
|
cursor++;
|
|
if (cursor == Entities.Count)
|
|
{
|
|
CompletedStages |= stage;
|
|
_entityCursors.Remove(stage);
|
|
_failures.Remove(stage);
|
|
}
|
|
else
|
|
{
|
|
_entityCursors[stage] = cursor;
|
|
}
|
|
|
|
return LandblockRetirementOperationResult.Progressed;
|
|
}
|
|
|
|
internal LandblockRetirementStage NextIncompleteStage
|
|
{
|
|
get
|
|
{
|
|
ushort remaining = (ushort)(RequiredStages & ~CompletedStages);
|
|
if (remaining == 0)
|
|
return LandblockRetirementStage.None;
|
|
|
|
ushort lowest = (ushort)(remaining & (ushort)-(short)remaining);
|
|
return (LandblockRetirementStage)lowest;
|
|
}
|
|
}
|
|
|
|
internal void BeginAttempt() => _callbackFailure = null;
|
|
|
|
internal void RecordCallbackFailure(Exception error) =>
|
|
_callbackFailure = error;
|
|
|
|
internal bool TryTakeNewFailure(out Exception? error)
|
|
{
|
|
error = _callbackFailure ?? _failures.Values.FirstOrDefault();
|
|
if (error is null || ReferenceEquals(error, _lastReportedFailure))
|
|
return false;
|
|
|
|
_lastReportedFailure = error;
|
|
return true;
|
|
}
|
|
|
|
private static void ValidateSingleStage(LandblockRetirementStage stage)
|
|
{
|
|
ushort value = (ushort)stage;
|
|
if (value == 0 || (value & (value - 1)) != 0)
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(stage),
|
|
stage,
|
|
"A retirement operation must own exactly one stage bit.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pipeline-lifetime owner for landblock retirement. World-state detachment
|
|
/// commits first; renderer/cache/script cleanup advances afterward from a
|
|
/// per-owner ledger. Reentrant begin/advance requests are serialized so an
|
|
/// owner callback cannot recursively replay its active stage or mutate the
|
|
/// pending-ticket map while it is being enumerated.
|
|
/// </summary>
|
|
public sealed class LandblockRetirementCoordinator
|
|
{
|
|
private enum RequestKind : byte
|
|
{
|
|
BeginFull,
|
|
BeginNearLayer,
|
|
Advance,
|
|
}
|
|
|
|
private readonly record struct Request(RequestKind Kind, uint LandblockId);
|
|
|
|
private const LandblockRetirementStage CoreStages =
|
|
LandblockRetirementStage.MeshReferences
|
|
| LandblockRetirementStage.StaticScripts
|
|
| LandblockRetirementStage.Classification;
|
|
|
|
public const LandblockRetirementStage ProductionPresentationStages =
|
|
LandblockRetirementStage.EntityLighting
|
|
| LandblockRetirementStage.EntityTranslucency
|
|
| LandblockRetirementStage.PluginProjection
|
|
| LandblockRetirementStage.Terrain
|
|
| LandblockRetirementStage.Physics
|
|
| LandblockRetirementStage.CellVisibility
|
|
| LandblockRetirementStage.BuildingRegistry
|
|
| LandblockRetirementStage.EnvironmentCells;
|
|
|
|
private readonly GpuWorldState _state;
|
|
private readonly Action<LandblockRetirementTicket> _advancePresentation;
|
|
private readonly Func<
|
|
LandblockRetirementTicket,
|
|
LandblockRetirementOperationResult>? _advancePresentationStep;
|
|
private readonly Func<LandblockRetirementKind, LandblockRetirementStage>
|
|
_requiredPresentationStages;
|
|
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
|
|
private readonly Queue<LandblockRetirementTicket> _pendingOrder = new();
|
|
private readonly List<uint> _completedIds = new();
|
|
private readonly Queue<Request> _requests = new();
|
|
private readonly HashSet<Request> _seenDuringDrain = new();
|
|
private bool _drainingRequests;
|
|
|
|
public LandblockRetirementCoordinator(
|
|
GpuWorldState state,
|
|
Action<LandblockRetirementTicket> advancePresentation,
|
|
Func<LandblockRetirementKind, LandblockRetirementStage>? requiredPresentationStages = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(state);
|
|
ArgumentNullException.ThrowIfNull(advancePresentation);
|
|
_state = state;
|
|
_advancePresentation = advancePresentation;
|
|
_advancePresentationStep = null;
|
|
_requiredPresentationStages = requiredPresentationStages
|
|
?? (kind => kind == LandblockRetirementKind.Full
|
|
? ProductionPresentationStages
|
|
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain);
|
|
}
|
|
|
|
private LandblockRetirementCoordinator(
|
|
GpuWorldState state,
|
|
Func<LandblockRetirementTicket, LandblockRetirementOperationResult>
|
|
advancePresentationStep,
|
|
Action<LandblockRetirementTicket> advancePresentation,
|
|
Func<LandblockRetirementKind, LandblockRetirementStage>
|
|
requiredPresentationStages)
|
|
{
|
|
_state = state ?? throw new ArgumentNullException(nameof(state));
|
|
_advancePresentationStep = advancePresentationStep
|
|
?? throw new ArgumentNullException(nameof(advancePresentationStep));
|
|
_advancePresentation = advancePresentation
|
|
?? throw new ArgumentNullException(nameof(advancePresentation));
|
|
_requiredPresentationStages = requiredPresentationStages
|
|
?? throw new ArgumentNullException(nameof(requiredPresentationStages));
|
|
}
|
|
|
|
public int PendingCount { get; private set; }
|
|
|
|
public bool IsPending(uint landblockId) =>
|
|
_pending.ContainsKey(Canonicalize(landblockId));
|
|
|
|
internal bool MatchesState(GpuWorldState state) =>
|
|
ReferenceEquals(_state, state);
|
|
internal bool UsesBudgetedSteps => _advancePresentationStep is not null;
|
|
|
|
public void BeginFull(uint landblockId) => EnqueueRequest(
|
|
new Request(RequestKind.BeginFull, Canonicalize(landblockId)));
|
|
|
|
public void BeginNearLayer(uint landblockId) => EnqueueRequest(
|
|
new Request(RequestKind.BeginNearLayer, Canonicalize(landblockId)));
|
|
|
|
public void Advance() => EnqueueRequest(
|
|
new Request(RequestKind.Advance, 0u));
|
|
|
|
internal static LandblockRetirementCoordinator CreateBudgeted(
|
|
GpuWorldState state,
|
|
Func<LandblockRetirementTicket, LandblockRetirementOperationResult>
|
|
advancePresentationStep,
|
|
Action<LandblockRetirementTicket> advancePresentation)
|
|
{
|
|
return new LandblockRetirementCoordinator(
|
|
state,
|
|
advancePresentationStep,
|
|
advancePresentation,
|
|
kind => kind == LandblockRetirementKind.Full
|
|
? ProductionPresentationStages
|
|
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances stable FIFO retirement work under the caller's one frame meter.
|
|
/// Every reservation maps to exactly one entity cursor or owner stage.
|
|
/// </summary>
|
|
public void Advance(StreamingWorkMeter meter)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(meter);
|
|
if (_advancePresentationStep is null)
|
|
{
|
|
Advance();
|
|
return;
|
|
}
|
|
|
|
while (_pendingOrder.TryPeek(out LandblockRetirementTicket? ticket))
|
|
{
|
|
if (ticket.IsComplete)
|
|
{
|
|
RemoveCompletedTicket(ticket);
|
|
continue;
|
|
}
|
|
|
|
LandblockRetirementStage stage = ticket.NextIncompleteStage;
|
|
if (stage == LandblockRetirementStage.None)
|
|
throw new InvalidOperationException(
|
|
"An incomplete retirement ticket has no unfinished stage.");
|
|
|
|
StreamingWorkCost cost = new(
|
|
EntityOperations: 1,
|
|
GlRetireOperations:
|
|
stage is LandblockRetirementStage.MeshReferences
|
|
or LandblockRetirementStage.Terrain
|
|
? 1
|
|
: 0);
|
|
StreamingWorkAdmission admission = meter.TryReserve(
|
|
cost,
|
|
$"retire-{stage}-0x{ticket.LandblockId:X8}");
|
|
if (admission == StreamingWorkAdmission.Yielded)
|
|
return;
|
|
|
|
LandblockRetirementOperationResult result = AdvanceTicketOne(ticket);
|
|
if (result == LandblockRetirementOperationResult.Failed)
|
|
{
|
|
meter.Fail();
|
|
return;
|
|
}
|
|
|
|
meter.Complete();
|
|
if (ticket.IsComplete)
|
|
RemoveCompletedTicket(ticket);
|
|
}
|
|
}
|
|
|
|
private void AdvanceCore()
|
|
{
|
|
if (_advancePresentationStep is not null)
|
|
{
|
|
AdvanceBudgetedEagerAttempt();
|
|
return;
|
|
}
|
|
|
|
if (_pending.Count == 0)
|
|
return;
|
|
|
|
_completedIds.Clear();
|
|
foreach ((uint id, List<LandblockRetirementTicket> tickets) in _pending)
|
|
{
|
|
for (int i = tickets.Count - 1; i >= 0; i--)
|
|
{
|
|
LandblockRetirementTicket ticket = tickets[i];
|
|
AdvanceTicket(ticket);
|
|
if (!ticket.IsComplete)
|
|
continue;
|
|
|
|
tickets.RemoveAt(i);
|
|
PendingCount--;
|
|
}
|
|
|
|
if (tickets.Count == 0)
|
|
_completedIds.Add(id);
|
|
}
|
|
|
|
for (int i = 0; i < _completedIds.Count; i++)
|
|
_pending.Remove(_completedIds[i]);
|
|
}
|
|
|
|
internal static LandblockRetirementCoordinator CreateLegacy(
|
|
GpuWorldState state,
|
|
Action<uint>? removeTerrain,
|
|
Action<uint>? demoteNearLayer)
|
|
{
|
|
LandblockRetirementStage Required(LandblockRetirementKind kind) =>
|
|
kind switch
|
|
{
|
|
LandblockRetirementKind.Full when removeTerrain is not null =>
|
|
LandblockRetirementStage.LegacyPresentation,
|
|
LandblockRetirementKind.NearLayer when demoteNearLayer is not null =>
|
|
LandblockRetirementStage.LegacyPresentation,
|
|
_ => LandblockRetirementStage.None,
|
|
};
|
|
|
|
void AdvanceLegacy(LandblockRetirementTicket ticket)
|
|
{
|
|
Action<uint>? callback = ticket.Kind == LandblockRetirementKind.Full
|
|
? removeTerrain
|
|
: demoteNearLayer;
|
|
if (callback is not null)
|
|
{
|
|
ticket.RunOnce(
|
|
LandblockRetirementStage.LegacyPresentation,
|
|
() => callback(ticket.LandblockId));
|
|
}
|
|
}
|
|
|
|
return new LandblockRetirementCoordinator(state, AdvanceLegacy, Required);
|
|
}
|
|
|
|
private void BeginCore(uint landblockId, LandblockRetirementKind kind)
|
|
{
|
|
uint canonical = Canonicalize(landblockId);
|
|
if (_pending.TryGetValue(canonical, out List<LandblockRetirementTicket>? existing))
|
|
{
|
|
for (int i = 0; i < existing.Count; i++)
|
|
{
|
|
if (existing[i].Kind == kind)
|
|
{
|
|
LandblockRetirementTicket existingTicket = existing[i];
|
|
if (_advancePresentationStep is not null)
|
|
return;
|
|
|
|
AdvanceTicket(existingTicket);
|
|
if (existingTicket.IsComplete)
|
|
{
|
|
existing.RemoveAt(i);
|
|
PendingCount--;
|
|
if (existing.Count == 0)
|
|
_pending.Remove(canonical);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
LandblockRetirementStage required =
|
|
CoreStages | _requiredPresentationStages(kind);
|
|
GpuLandblockRetirement? stateRetirement = null;
|
|
Exception? detachmentObserverFailure = null;
|
|
GpuWorldState.MutationBatch mutation = _state.BeginMutationBatch();
|
|
try
|
|
{
|
|
// The detach methods create a nested batch. Holding this outer
|
|
// transaction lets us retain their returned teardown context
|
|
// before visibility observers run at the outer commit boundary.
|
|
stateRetirement = kind == LandblockRetirementKind.Full
|
|
? _state.DetachLandblock(canonical)
|
|
: _state.DetachNearLayer(canonical);
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
mutation.Dispose();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
detachmentObserverFailure = error;
|
|
}
|
|
}
|
|
if (stateRetirement is null)
|
|
{
|
|
if (detachmentObserverFailure is not null)
|
|
throw detachmentObserverFailure;
|
|
return;
|
|
}
|
|
|
|
var ticket = new LandblockRetirementTicket(stateRetirement, required);
|
|
if (existing is null)
|
|
{
|
|
existing = new List<LandblockRetirementTicket>(1);
|
|
_pending.Add(canonical, existing);
|
|
}
|
|
existing.Add(ticket);
|
|
PendingCount++;
|
|
if (_advancePresentationStep is not null)
|
|
_pendingOrder.Enqueue(ticket);
|
|
|
|
if (_advancePresentationStep is null)
|
|
{
|
|
AdvanceTicket(ticket);
|
|
if (ticket.IsComplete)
|
|
{
|
|
existing.Remove(ticket);
|
|
PendingCount--;
|
|
if (existing.Count == 0)
|
|
_pending.Remove(canonical);
|
|
}
|
|
}
|
|
// Visibility is a delivered-by-attempt broadcast: every subscriber is
|
|
// invoked and failures are aggregated, but failed subscribers are not
|
|
// retained as replayable work. Complete the presentation teardown
|
|
// from the preserved detach receipt, then surface that committed-edge
|
|
// failure to the caller without inventing a one-frame pending fence.
|
|
if (detachmentObserverFailure is not null)
|
|
throw detachmentObserverFailure;
|
|
}
|
|
|
|
private void AdvanceTicket(LandblockRetirementTicket ticket)
|
|
{
|
|
ticket.BeginAttempt();
|
|
ticket.RunOnce(
|
|
LandblockRetirementStage.MeshReferences,
|
|
() => _state.ReleaseLandblockMeshReferences(ticket.LandblockId));
|
|
ticket.RunForEachEntity(
|
|
LandblockRetirementStage.StaticScripts,
|
|
static entity => entity.ServerGuid == 0,
|
|
_state.StopStaticEntityScript);
|
|
ticket.RunOnce(
|
|
LandblockRetirementStage.Classification,
|
|
() => _state.InvalidateLandblockClassification(ticket.LandblockId));
|
|
|
|
try
|
|
{
|
|
_advancePresentation(ticket);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
ticket.RecordCallbackFailure(error);
|
|
}
|
|
|
|
if (ticket.TryTakeNewFailure(out Exception? failure))
|
|
{
|
|
Console.WriteLine(
|
|
$"streaming: retirement for 0x{ticket.LandblockId:X8} " +
|
|
$"({ticket.Kind}) remains pending: {failure}");
|
|
}
|
|
}
|
|
|
|
private void AdvanceBudgetedEagerAttempt()
|
|
{
|
|
if (!_pendingOrder.TryPeek(out LandblockRetirementTicket? ticket))
|
|
return;
|
|
|
|
if (ticket.IsComplete)
|
|
{
|
|
RemoveCompletedTicket(ticket);
|
|
return;
|
|
}
|
|
|
|
// Compatibility/debug callers explicitly asking for an unmetered
|
|
// advance retain the former one-attempt behavior: every independent
|
|
// stage is attempted once, while successful stages/entities remain
|
|
// committed in the same exact ticket. Production calls the metered
|
|
// overload and never enters this path.
|
|
AdvanceTicket(ticket);
|
|
if (ticket.IsComplete)
|
|
RemoveCompletedTicket(ticket);
|
|
}
|
|
|
|
private LandblockRetirementOperationResult AdvanceTicketOne(
|
|
LandblockRetirementTicket ticket)
|
|
{
|
|
ticket.BeginAttempt();
|
|
LandblockRetirementStage stage = ticket.NextIncompleteStage;
|
|
LandblockRetirementOperationResult result;
|
|
try
|
|
{
|
|
result = stage switch
|
|
{
|
|
LandblockRetirementStage.MeshReferences =>
|
|
ticket.RunOnceStep(
|
|
stage,
|
|
() => _state.ReleaseLandblockMeshReferences(
|
|
ticket.LandblockId)),
|
|
LandblockRetirementStage.StaticScripts =>
|
|
ticket.RunEntityStep(
|
|
stage,
|
|
static entity => entity.ServerGuid == 0,
|
|
_state.StopStaticEntityScript),
|
|
LandblockRetirementStage.Classification =>
|
|
ticket.RunOnceStep(
|
|
stage,
|
|
() => _state.InvalidateLandblockClassification(
|
|
ticket.LandblockId)),
|
|
LandblockRetirementStage.None =>
|
|
LandblockRetirementOperationResult.NoWork,
|
|
_ => _advancePresentationStep!(ticket),
|
|
};
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
ticket.RecordCallbackFailure(error);
|
|
result = LandblockRetirementOperationResult.Failed;
|
|
}
|
|
|
|
if (ticket.TryTakeNewFailure(out Exception? failure))
|
|
{
|
|
Console.WriteLine(
|
|
$"streaming: retirement for 0x{ticket.LandblockId:X8} " +
|
|
$"({ticket.Kind}) remains pending: {failure}");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void RemoveCompletedTicket(LandblockRetirementTicket ticket)
|
|
{
|
|
if (!_pendingOrder.TryPeek(out LandblockRetirementTicket? head)
|
|
|| !ReferenceEquals(head, ticket))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Retirement FIFO completion did not match its active head.");
|
|
}
|
|
_pendingOrder.Dequeue();
|
|
|
|
uint id = Canonicalize(ticket.LandblockId);
|
|
if (!_pending.TryGetValue(id, out List<LandblockRetirementTicket>? tickets)
|
|
|| !tickets.Remove(ticket))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Completed retirement ticket is missing from its owner ledger.");
|
|
}
|
|
|
|
PendingCount--;
|
|
if (tickets.Count == 0)
|
|
_pending.Remove(id);
|
|
}
|
|
|
|
private static uint Canonicalize(uint id) =>
|
|
(id & 0xFFFF0000u) | 0xFFFFu;
|
|
|
|
private void EnqueueRequest(Request request)
|
|
{
|
|
if (_drainingRequests)
|
|
{
|
|
if (_seenDuringDrain.Add(request))
|
|
_requests.Enqueue(request);
|
|
return;
|
|
}
|
|
|
|
_drainingRequests = true;
|
|
_seenDuringDrain.Clear();
|
|
_requests.Clear();
|
|
_seenDuringDrain.Add(request);
|
|
_requests.Enqueue(request);
|
|
List<Exception>? failures = null;
|
|
try
|
|
{
|
|
while (_requests.TryDequeue(out Request pending))
|
|
{
|
|
try
|
|
{
|
|
switch (pending.Kind)
|
|
{
|
|
case RequestKind.BeginFull:
|
|
BeginCore(
|
|
pending.LandblockId,
|
|
LandblockRetirementKind.Full);
|
|
break;
|
|
case RequestKind.BeginNearLayer:
|
|
BeginCore(
|
|
pending.LandblockId,
|
|
LandblockRetirementKind.NearLayer);
|
|
break;
|
|
case RequestKind.Advance:
|
|
AdvanceCore();
|
|
break;
|
|
default:
|
|
throw new InvalidOperationException(
|
|
$"Unknown retirement request {pending.Kind}.");
|
|
}
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= new List<Exception>()).Add(error);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_requests.Clear();
|
|
_seenDuringDrain.Clear();
|
|
_drainingRequests = false;
|
|
}
|
|
|
|
if (failures is { Count: 1 })
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
|
.Capture(failures[0])
|
|
.Throw();
|
|
if (failures is { Count: > 1 })
|
|
{
|
|
throw new AggregateException(
|
|
"One or more serialized landblock retirement requests failed.",
|
|
failures);
|
|
}
|
|
}
|
|
}
|