using System.Linq; using Content.Shared.Examine; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Timing; namespace Content.Client.DoAfter { /// /// Handles events that need to happen after a certain amount of time where the event could be cancelled by factors /// such as moving. /// [UsedImplicitly] public sealed class DoAfterSystem : EntitySystem { /* * How this is currently setup (client-side): * DoAfterGui handles the actual bars displayed above heads. It also uses FrameUpdate to flash cancellations * DoAfterEntitySystem handles checking predictions every tick as well as removing / cancelling DoAfters due to time elapsed. * DoAfterComponent handles network messages inbound as well as storing the DoAfter data. * It'll also handle overall cleanup when one is removed (i.e. removing it from DoAfterGui). */ [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; /// /// We'll use an excess time so stuff like finishing effects can show. /// public const float ExcessTime = 0.5f; private EntityUid? _attachedEntity; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(HandlePlayerAttached); } private void HandlePlayerAttached(PlayerAttachSysMessage message) { _attachedEntity = message.AttachedEntity; } public override void Update(float frameTime) { base.Update(frameTime); var currentTime = _gameTiming.CurTime; // Can't see any I guess? if (_attachedEntity is not {Valid: true} entity || Deleted(entity)) return; var viewbox = _eyeManager.GetWorldViewport().Enlarged(2.0f); foreach (var comp in EntityManager.EntityQuery(true)) { var doAfters = comp.DoAfters.ToList(); var compPos = EntityManager.GetComponent(comp.Owner).WorldPosition; if (doAfters.Count == 0 || EntityManager.GetComponent(comp.Owner).MapID != EntityManager.GetComponent(entity).MapID || !viewbox.Contains(compPos)) { comp.Disable(); continue; } var range = (compPos - EntityManager.GetComponent(entity).WorldPosition).Length + 0.01f; if (comp.Owner != _attachedEntity && !ExamineSystemShared.InRangeUnOccluded( EntityManager.GetComponent(entity).MapPosition, EntityManager.GetComponent(comp.Owner).MapPosition, range, entity => entity == comp.Owner || entity == _attachedEntity)) { comp.Disable(); continue; } comp.Enable(); var userGrid = EntityManager.GetComponent(comp.Owner).Coordinates; // Check cancellations / finishes foreach (var (id, doAfter) in doAfters) { var elapsedTime = (currentTime - doAfter.StartTime).TotalSeconds; // If we've passed the final time (after the excess to show completion graphic) then remove. if (elapsedTime > doAfter.Delay + ExcessTime) { comp.Remove(doAfter); continue; } // Don't predict cancellation if it's already finished. if (elapsedTime > doAfter.Delay) { continue; } // Predictions if (doAfter.BreakOnUserMove) { if (!userGrid.InRange(EntityManager, doAfter.UserGrid, doAfter.MovementThreshold)) { comp.Cancel(id, currentTime); continue; } } if (doAfter.BreakOnTargetMove) { if (EntityManager.EntityExists(doAfter.TargetUid) && !EntityManager.GetComponent(doAfter.TargetUid).Coordinates.InRange(EntityManager, doAfter.TargetGrid, doAfter.MovementThreshold)) { comp.Cancel(id, currentTime); continue; } } } var count = comp.CancelledDoAfters.Count; // Remove cancelled DoAfters after ExcessTime has elapsed for (var i = count - 1; i >= 0; i--) { var cancelled = comp.CancelledDoAfters[i]; if ((currentTime - cancelled.CancelTime).TotalSeconds > ExcessTime) { comp.Remove(cancelled.Message); } } } } } }