using System.Linq; using Content.Server.Worldgen.Components.GC; using Content.Server.Worldgen.Prototypes; using Content.Shared.CCVar; using JetBrains.Annotations; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server.Worldgen.Systems.GC; /// /// This handles delayed garbage collection of entities, to avoid overloading the tick in particularly expensive cases. /// public sealed class GCQueueSystem : EntitySystem { [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IRobustRandom _random = default!; [ViewVariables] private TimeSpan _maximumProcessTime = TimeSpan.Zero; [ViewVariables] private readonly Dictionary> _queues = new(); /// public override void Initialize() { _cfg.OnValueChanged(CCVars.GCMaximumTimeMs, s => _maximumProcessTime = TimeSpan.FromMilliseconds(s), true); } /// CCVars public override void Update(float frameTime) { var overallWatch = new Stopwatch(); var queueWatch = new Stopwatch(); var queues = _queues.ToList(); _random.Shuffle(queues); // Avert resource starvation by always processing in random order. overallWatch.Start(); foreach (var (pId, queue) in queues) { if (overallWatch.Elapsed > _maximumProcessTime) return; var proto = _proto.Index(pId); if (queue.Count < proto.MinDepthToProcess) continue; queueWatch.Restart(); while (queueWatch.Elapsed < proto.MaximumTickTime && queue.Count >= proto.MinDepthToProcess && overallWatch.Elapsed < _maximumProcessTime) { var e = queue.Dequeue(); if (!Deleted(e)) { var ev = new TryCancelGC(); RaiseLocalEvent(e, ref ev); if (!ev.Cancelled) Del(e); } } } } /// /// Attempts to GC an entity. This functions as QueueDel if it can't. /// /// Entity to GC. public void TryGCEntity(EntityUid e) { if (!TryComp(e, out var comp)) { QueueDel(e); // not our problem :) return; } if (!_queues.TryGetValue(comp.Queue, out var queue)) { queue = new Queue(); _queues[comp.Queue] = queue; } var proto = _proto.Index(comp.Queue); if (queue.Count > proto.Depth) { QueueDel(e); // whelp, too full. return; } if (proto.TrySkipQueue) { var ev = new TryGCImmediately(); RaiseLocalEvent(e, ref ev); if (!ev.Cancelled) { QueueDel(e); return; } } queue.Enqueue(e); } } /// /// Fired by GCQueueSystem to check if it can simply immediately GC an entity, for example if it was never fully /// loaded. /// /// Whether or not the immediate deletion attempt was cancelled. [ByRefEvent] [PublicAPI] public record struct TryGCImmediately(bool Cancelled = false); /// /// Fired by GCQueueSystem to check if the collection of the given entity should be cancelled, for example it's chunk /// being loaded again. /// /// Whether or not the deletion attempt was cancelled. [ByRefEvent] [PublicAPI] public record struct TryCancelGC(bool Cancelled = false);