using System.Linq; using System.Threading.Tasks; using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.MobState; using JetBrains.Annotations; using Robust.Shared.GameStates; namespace Content.Server.DoAfter { [UsedImplicitly] public sealed class DoAfterSystem : EntitySystem { // We cache these lists as to not allocate them every update tick... private readonly Queue _cancelled = new(); private readonly Queue _finished = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnDamage); SubscribeLocalEvent(OnStateChanged); SubscribeLocalEvent(OnDoAfterGetState); } public void Add(DoAfterComponent component, DoAfter doAfter) { component.DoAfters.Add(doAfter, component.RunningIndex); component.RunningIndex++; Dirty(component); } public void Cancelled(DoAfterComponent component, DoAfter doAfter) { if (!component.DoAfters.TryGetValue(doAfter, out var index)) return; component.DoAfters.Remove(doAfter); RaiseNetworkEvent(new CancelledDoAfterMessage(component.Owner, index)); } /// /// Call when the particular DoAfter is finished. /// Client should be tracking this independently. /// public void Finished(DoAfterComponent component, DoAfter doAfter) { if (!component.DoAfters.ContainsKey(doAfter)) return; component.DoAfters.Remove(doAfter); } private void OnDoAfterGetState(EntityUid uid, DoAfterComponent component, ref ComponentGetState args) { var toAdd = new List(component.DoAfters.Count); foreach (var (doAfter, _) in component.DoAfters) { // THE ALMIGHTY PYRAMID var clientDoAfter = new ClientDoAfter( component.DoAfters[doAfter], doAfter.UserGrid, doAfter.TargetGrid, doAfter.StartTime, doAfter.EventArgs.Delay, doAfter.EventArgs.BreakOnUserMove, doAfter.EventArgs.BreakOnTargetMove, doAfter.EventArgs.MovementThreshold, doAfter.EventArgs.Target); toAdd.Add(clientDoAfter); } args.State = new DoAfterComponentState(toAdd); } private void OnStateChanged(EntityUid uid, DoAfterComponent component, MobStateChangedEvent args) { if (!args.CurrentMobState.IsIncapacitated()) return; foreach (var (doAfter, _) in component.DoAfters) { doAfter.Cancel(); } } public void OnDamage(EntityUid _, DoAfterComponent component, DamageChangedEvent args) { if (!args.InterruptsDoAfters || !args.DamageIncreased) return; foreach (var (doAfter, _) in component.DoAfters) { if (doAfter.EventArgs.BreakOnDamage) { doAfter.Cancel(); } } } public override void Update(float frameTime) { base.Update(frameTime); foreach (var comp in EntityManager.EntityQuery()) { foreach (var (doAfter, _) in comp.DoAfters.ToArray()) { doAfter.Run(frameTime, EntityManager); switch (doAfter.Status) { case DoAfterStatus.Running: break; case DoAfterStatus.Cancelled: _cancelled.Enqueue(doAfter); break; case DoAfterStatus.Finished: _finished.Enqueue(doAfter); break; default: throw new ArgumentOutOfRangeException(); } } while (_cancelled.TryDequeue(out var doAfter)) { Cancelled(comp, doAfter); if(EntityManager.EntityExists(doAfter.EventArgs.User) && doAfter.EventArgs.UserCancelledEvent != null) RaiseLocalEvent(doAfter.EventArgs.User, doAfter.EventArgs.UserCancelledEvent, false); if(doAfter.EventArgs.Target is {} target && EntityManager.EntityExists(target) && doAfter.EventArgs.TargetCancelledEvent != null) RaiseLocalEvent(target, doAfter.EventArgs.TargetCancelledEvent, false); if(doAfter.EventArgs.BroadcastCancelledEvent != null) RaiseLocalEvent(doAfter.EventArgs.BroadcastCancelledEvent); } while (_finished.TryDequeue(out var doAfter)) { Finished(comp, doAfter); if(EntityManager.EntityExists(doAfter.EventArgs.User) && doAfter.EventArgs.UserFinishedEvent != null) RaiseLocalEvent(doAfter.EventArgs.User, doAfter.EventArgs.UserFinishedEvent, false); if(doAfter.EventArgs.Target is {} target && EntityManager.EntityExists(target) && doAfter.EventArgs.TargetFinishedEvent != null) RaiseLocalEvent(target, doAfter.EventArgs.TargetFinishedEvent, false); if(doAfter.EventArgs.BroadcastFinishedEvent != null) RaiseLocalEvent(doAfter.EventArgs.BroadcastFinishedEvent); } } } /// /// Tasks that are delayed until the specified time has passed /// These can be potentially cancelled by the user moving or when other things happen. /// /// /// public async Task WaitDoAfter(DoAfterEventArgs eventArgs) { var doAfter = CreateDoAfter(eventArgs); await doAfter.AsTask; return doAfter.Status; } /// /// Creates a DoAfter without waiting for it to finish. You can use events with this. /// These can be potentially cancelled by the user moving or when other things happen. /// /// public void DoAfter(DoAfterEventArgs eventArgs) { CreateDoAfter(eventArgs); } private DoAfter CreateDoAfter(DoAfterEventArgs eventArgs) { // Setup var doAfter = new DoAfter(eventArgs, EntityManager); // Caller's gonna be responsible for this I guess var doAfterComponent = Comp(eventArgs.User); Add(doAfterComponent, doAfter); return doAfter; } } public enum DoAfterStatus : byte { Running, Cancelled, Finished, } }