diff --git a/Content.Client/DragDrop/DragDropSystem.cs b/Content.Client/DragDrop/DragDropSystem.cs index e671e4c7a8..5bacb08bff 100644 --- a/Content.Client/DragDrop/DragDropSystem.cs +++ b/Content.Client/DragDrop/DragDropSystem.cs @@ -80,9 +80,8 @@ namespace Content.Client.DragDrop _dropTargetOutOfRangeShader = _prototypeManager.Index(ShaderDropTargetOutOfRange).Instance(); // needs to fire on mouseup and mousedown so we can detect a drag / drop CommandBinds.Builder - .Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false)) + .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false), new[] { typeof(SharedInteractionSystem) }) .Register(); - } public override void Shutdown() diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index 033a71a385..aa6c405370 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -44,6 +44,16 @@ public sealed class DoorSystem : SharedDoorSystem SubscribeLocalEvent(OnEmagged); } + protected override void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args) + { + // TODO once access permissions are shared, move this back to shared. + if (args.Handled || !door.ClickOpen) + return; + + TryToggleDoor(uid, door, args.User); + args.Handled = true; + } + protected override void SetCollidable(EntityUid uid, bool collidable, DoorComponent? door = null, PhysicsComponent? physics = null, diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index a748a77ffd..f30ba3061b 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Threading.Tasks; using Content.Server.Administration.Logs; @@ -6,13 +5,11 @@ using Content.Server.CombatMode; using Content.Server.Hands.Components; using Content.Server.Pulling; using Content.Server.Storage.Components; -using Content.Server.Timing; using Content.Shared.ActionBlocker; using Content.Shared.Database; using Content.Shared.DragDrop; using Content.Shared.Input; using Content.Shared.Interaction; -using Content.Shared.Interaction.Helpers; using Content.Shared.Item; using Content.Shared.Pulling.Components; using Content.Shared.Timing; @@ -20,11 +17,7 @@ using Content.Shared.Weapons.Melee; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Players; @@ -39,7 +32,6 @@ namespace Content.Server.Interaction [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly PullingSystem _pullSystem = default!; [Dependency] private readonly AdminLogSystem _adminLogSystem = default!; - [Dependency] private readonly UseDelaySystem _useDelay = default!; public override void Initialize() { @@ -48,12 +40,8 @@ namespace Content.Server.Interaction SubscribeNetworkEvent(HandleDragDropRequestEvent); CommandBinds.Builder - .Bind(EngineKeyFunctions.Use, - new PointerInputCmdHandler(HandleUseInteraction)) .Bind(ContentKeyFunctions.WideAttack, new PointerInputCmdHandler(HandleWideAttack)) - .Bind(ContentKeyFunctions.ActivateItemInWorld, - new PointerInputCmdHandler(HandleActivateItemInWorld)) .Bind(ContentKeyFunctions.TryPullObject, new PointerInputCmdHandler(HandleTryPullObject)) .Register(); @@ -86,11 +74,6 @@ namespace Content.Server.Interaction return storage.SubscribedSessions.Contains(actor.PlayerSession); } - protected override void BeginDelay(UseDelayComponent? component = null) - { - _useDelay.BeginDelay(component); - } - #region Drag drop private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args) { @@ -146,23 +129,6 @@ namespace Content.Server.Interaction } #endregion - #region ActivateItemInWorld - private bool HandleActivateItemInWorld(ICommonSession? session, EntityCoordinates coords, EntityUid uid) - { - if (!ValidateClientInput(session, coords, uid, out var user)) - { - Logger.InfoS("system.interaction", $"ActivateItemInWorld input validation failed"); - return false; - } - - if (Deleted(uid)) - return false; - - InteractionActivate(user.Value, uid); - return true; - } - #endregion - private bool HandleWideAttack(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { // client sanitization @@ -193,20 +159,6 @@ namespace Content.Server.Interaction UserInteraction(entity, coords, uid); } - public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid) - { - // client sanitization - if (!ValidateClientInput(session, coords, uid, out var userEntity)) - { - Logger.InfoS("system.interaction", $"Use input validation failed"); - return true; - } - - UserInteraction(userEntity.Value, coords, !Deleted(uid) ? uid : null); - - return true; - } - private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { if (!ValidateClientInput(session, coords, uid, out var userEntity)) @@ -230,68 +182,6 @@ namespace Content.Server.Interaction return _pullSystem.TogglePull(userEntity.Value, pull); } - /// - /// Uses an empty hand on an entity - /// Finds components with the InteractHand interface and calls their function - /// NOTE: Does not have any range or can-interact checks. These should all have been done before this function is called. - /// - public override void InteractHand(EntityUid user, EntityUid target) - { - // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. - - // all interactions should only happen when in range / unobstructed, so no range check is needed - var message = new InteractHandEvent(user, target); - RaiseLocalEvent(target, message); - _adminLogSystem.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}"); - if (message.Handled) - return; - - var interactHandEventArgs = new InteractHandEventArgs(user, target); - - var interactHandComps = AllComps(target).ToList(); - foreach (var interactHandComp in interactHandComps) - { - // If an InteractHand returns a status completion we finish our interaction -#pragma warning disable 618 - if (interactHandComp.InteractHand(interactHandEventArgs)) -#pragma warning restore 618 - return; - } - - // Else we run Activate. - InteractionActivate(user, target, - checkCanInteract: false, - checkUseDelay: true, - checkAccess: false); - } - - /// - /// Will have two behaviors, either "uses" the used entity at range on the target entity if it is capable of accepting that action - /// Or it will use the used entity itself on the position clicked, regardless of what was there - /// - public override void InteractUsingRanged( - EntityUid user, - EntityUid used, - EntityUid? target, - EntityCoordinates clickLocation, - bool inRangeUnobstructed) - { - // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. - if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed)) - return; - - if (target != null) - { - var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation); - RaiseLocalEvent(target.Value, rangedMsg); - - if (rangedMsg.Handled) - return; - } - - InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed); - } - public override void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid? target = null) { // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. diff --git a/Content.Server/Timing/UseDelaySystem.cs b/Content.Server/Timing/UseDelaySystem.cs deleted file mode 100644 index aa9f6977a8..0000000000 --- a/Content.Server/Timing/UseDelaySystem.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Content.Shared.Cooldown; -using Content.Shared.Timing; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Timing; -using Robust.Shared.Utility; - -namespace Content.Server.Timing; - -public sealed class UseDelaySystem : EntitySystem -{ - [Dependency] private readonly IGameTiming _gameTiming = default!; - - private HashSet _activeDelays = new(); - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var toRemove = new RemQueue(); - - foreach (var delay in _activeDelays) - { - MetaDataComponent? metaData = null; - - if (Deleted(delay.Owner, metaData) || - delay.CancellationTokenSource?.Token.IsCancellationRequested == true) - { - toRemove.Add(delay); - continue; - } - - if (Paused(delay.Owner, metaData)) continue; - - delay.Elapsed += frameTime; - - if (delay.Elapsed < delay.Delay) continue; - - toRemove.Add(delay); - - } - - foreach (var delay in toRemove) - { - delay.CancellationTokenSource = null; - delay.Elapsed = 0f; - _activeDelays.Remove(delay); - } - } - - public void BeginDelay(UseDelayComponent? component = null) - { - if (component == null || - component.ActiveDelay || - Deleted(component.Owner)) return; - - component.CancellationTokenSource = new CancellationTokenSource(); - - DebugTools.Assert(!_activeDelays.Contains(component)); - _activeDelays.Add(component); - - var currentTime = _gameTiming.CurTime; - component.LastUseTime = currentTime; - - var cooldown = EnsureComp(component.Owner); - cooldown.CooldownStart = currentTime; - cooldown.CooldownEnd = currentTime + TimeSpan.FromSeconds(component.Delay); - } - - public void Cancel(UseDelayComponent component) - { - component.CancellationTokenSource?.Cancel(); - component.CancellationTokenSource = null; - - if (TryComp(component.Owner, out var cooldown)) - { - cooldown.CooldownEnd = _gameTiming.CurTime; - } - } - - public void Restart(UseDelayComponent component) - { - component.CancellationTokenSource?.Cancel(); - component.CancellationTokenSource = null; - BeginDelay(component); - } -} diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs index 1229db9685..3b7b850d82 100644 --- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs @@ -162,13 +162,10 @@ public abstract class SharedDoorSystem : EntitySystem #endregion #region Interactions - private void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args) + protected virtual void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args) { - if (args.Handled || !door.ClickOpen) - return; - - TryToggleDoor(uid, door, args.User); - args.Handled = true; + // avoid client-mispredicts, as the server will definitely handle this event + args.Handled = true; } private void OnExamine(EntityUid uid, DoorComponent door, ExaminedEvent args) diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 253b77b6f3..52f13ce7c3 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -8,7 +8,6 @@ using Content.Shared.CombatMode; using Content.Shared.Database; using Content.Shared.Hands.Components; using Content.Shared.Input; -using Content.Shared.Interaction.Helpers; using Content.Shared.Interaction.Components; using Content.Shared.Physics; using Content.Shared.Popups; @@ -25,6 +24,7 @@ using Robust.Shared.Serialization; using Content.Shared.Wall; using Content.Shared.Item; using Robust.Shared.Player; +using Robust.Shared.Input; #pragma warning disable 618 @@ -43,6 +43,7 @@ namespace Content.Shared.Interaction [Dependency] private readonly SharedAdminLogSystem _adminLogSystem = default!; [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!; public const float InteractionRange = 2; @@ -61,6 +62,10 @@ namespace Content.Shared.Interaction CommandBinds.Builder .Bind(ContentKeyFunctions.AltActivateItemInWorld, new PointerInputCmdHandler(HandleAltUseInteraction)) + .Bind(EngineKeyFunctions.Use, + new PointerInputCmdHandler(HandleUseInteraction)) + .Bind(ContentKeyFunctions.ActivateItemInWorld, + new PointerInputCmdHandler(HandleActivateItemInWorld)) .Register(); } @@ -144,6 +149,20 @@ namespace Content.Shared.Interaction return false; } + public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + { + // client sanitization + if (!ValidateClientInput(session, coords, uid, out var userEntity)) + { + Logger.InfoS("system.interaction", $"Use input validation failed"); + return true; + } + + UserInteraction(userEntity.Value, coords, !Deleted(uid) ? uid : null); + + return false; + } + /// /// Resolves user interactions with objects. /// @@ -243,9 +262,31 @@ namespace Content.Shared.Interaction inRangeUnobstructed); } - public virtual void InteractHand(EntityUid user, EntityUid target) + public void InteractHand(EntityUid user, EntityUid target) { - // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. + // all interactions should only happen when in range / unobstructed, so no range check is needed + var message = new InteractHandEvent(user, target); + RaiseLocalEvent(target, message); + _adminLogSystem.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}"); + if (message.Handled) + return; + + var interactHandEventArgs = new InteractHandEventArgs(user, target); + var interactHandComps = AllComps(target).ToList(); + foreach (var interactHandComp in interactHandComps) + { + // If an InteractHand returns a status completion we finish our interaction +#pragma warning disable 618 + if (interactHandComp.InteractHand(interactHandEventArgs)) +#pragma warning restore 618 + return; + } + + // Else we run Activate. + InteractionActivate(user, target, + checkCanInteract: false, + checkUseDelay: true, + checkAccess: false); } public virtual void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, @@ -254,10 +295,22 @@ namespace Content.Shared.Interaction // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. } - public virtual void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target, + public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) { - // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. + if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed)) + return; + + if (target != null) + { + var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation); + RaiseLocalEvent(target.Value, rangedMsg); + + if (rangedMsg.Handled) + return; + } + + InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed); } protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates) @@ -594,8 +647,8 @@ namespace Content.Shared.Interaction return; var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target); - var interactUsings = AllComps(target).OrderByDescending(x => x.Priority); + foreach (var interactUsing in interactUsings) { // If an InteractUsing returns a status completion we finish our interaction @@ -636,6 +689,21 @@ namespace Content.Shared.Interaction } #region ActivateItemInWorld + private bool HandleActivateItemInWorld(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + { + if (!ValidateClientInput(session, coords, uid, out var user)) + { + Logger.InfoS("system.interaction", $"ActivateItemInWorld input validation failed"); + return false; + } + + if (Deleted(uid)) + return false; + + InteractionActivate(user.Value, uid); + return false; + } + /// /// Raises events and activates the IActivate behavior of an object. /// @@ -671,18 +739,20 @@ namespace Content.Shared.Interaction RaiseLocalEvent(used, activateMsg); if (activateMsg.Handled) { - BeginDelay(delayComponent); + _useDelay.BeginDelay(used, delayComponent); _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}"); return true; } - - if (!TryComp(used, out IActivate? activateComp)) + + var activatable = AllComps(used).FirstOrDefault(); + if (activatable == null) return false; - var activateEventArgs = new ActivateEventArgs(user, used); - activateComp.Activate(activateEventArgs); - BeginDelay(delayComponent); - _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}"); // No way to check success. + activatable.Activate(new ActivateEventArgs(user, used)); + + // No way to check success. + _useDelay.BeginDelay(used, delayComponent); + _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}"); return true; } #endregion @@ -718,7 +788,7 @@ namespace Content.Shared.Interaction RaiseLocalEvent(used, useMsg); if (useMsg.Handled) { - BeginDelay(delayComponent); + _useDelay.BeginDelay(used, delayComponent); return true; } @@ -730,7 +800,7 @@ namespace Content.Shared.Interaction // If a Use returns a status completion we finish our interaction if (use.UseEntity(new UseEntityEventArgs(user))) { - BeginDelay(delayComponent); + _useDelay.BeginDelay(used, delayComponent); return true; } } @@ -739,12 +809,6 @@ namespace Content.Shared.Interaction return InteractionActivate(user, used, false, false, false); } - protected virtual void BeginDelay(UseDelayComponent? component = null) - { - // This is temporary until we have predicted UseDelay. - return; - } - /// /// Alternative interactions on an entity. /// diff --git a/Content.Shared/Item/SharedItemComponent.cs b/Content.Shared/Item/SharedItemComponent.cs index a697f1fda7..83c3b63fc9 100644 --- a/Content.Shared/Item/SharedItemComponent.cs +++ b/Content.Shared/Item/SharedItemComponent.cs @@ -20,7 +20,7 @@ namespace Content.Shared.Item /// Players can pick up, drop, and put items in bags, and they can be seen in player's hands. /// [NetworkedComponent()] - public abstract class SharedItemComponent : Component, IInteractHand + public abstract class SharedItemComponent : Component { [Dependency] private readonly IEntityManager _entMan = default!; @@ -97,22 +97,6 @@ namespace Content.Shared.Item [DataField("sprite")] public readonly string? RsiPath; - bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) - { - var user = eventArgs.User; - - if (!_entMan.TryGetComponent(user, out SharedHandsComponent hands)) - return false; - - var activeHand = hands.ActiveHand; - - if (activeHand == null) - return false; - - // hands checks action blockers - return hands.TryPickupEntityToActiveHand(Owner, animateUser: true); - } - public void RemovedFromSlot() { if (_entMan.TryGetComponent(Owner, out SharedSpriteComponent component)) diff --git a/Content.Shared/Item/SharedItemSystem.cs b/Content.Shared/Item/SharedItemSystem.cs index 88de904c8e..5379c5dfb2 100644 --- a/Content.Shared/Item/SharedItemSystem.cs +++ b/Content.Shared/Item/SharedItemSystem.cs @@ -1,3 +1,5 @@ +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; using Content.Shared.Inventory.Events; using Content.Shared.Verbs; using Robust.Shared.Containers; @@ -17,11 +19,26 @@ namespace Content.Shared.Item SubscribeLocalEvent(OnEquipped); SubscribeLocalEvent(OnUnequipped); + SubscribeLocalEvent(OnHandInteract); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); } + private void OnHandInteract(EntityUid uid, SharedItemComponent component, InteractHandEvent args) + { + if (args.Handled) + return; + + if (!TryComp(args.User, out SharedHandsComponent? hands)) + return; + + if (hands.ActiveHand == null) + return; + + args.Handled = hands.TryPickupEntity(hands.ActiveHand, uid, false, animateUser: false); + } + private void OnHandleState(EntityUid uid, SharedItemComponent component, ref ComponentHandleState args) { if (args.Current is not ItemComponentState state) diff --git a/Content.Shared/Timing/UseDelayComponent.cs b/Content.Shared/Timing/UseDelayComponent.cs index 5827daef39..e6884ebee2 100644 --- a/Content.Shared/Timing/UseDelayComponent.cs +++ b/Content.Shared/Timing/UseDelayComponent.cs @@ -1,8 +1,6 @@ -using System; using System.Threading; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.ViewVariables; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; namespace Content.Shared.Timing { @@ -10,20 +8,40 @@ namespace Content.Shared.Timing /// Timer that creates a cooldown each time an object is activated/used /// [RegisterComponent] + [NetworkedComponent] public sealed class UseDelayComponent : Component { - [ViewVariables] public TimeSpan LastUseTime; - [ViewVariables] - [DataField("delay")] - public float Delay = 1; + public TimeSpan? DelayEndTime; - [ViewVariables] - public float Elapsed = 0f; + [DataField("delay")] + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan Delay = TimeSpan.FromSeconds(1); + + /// + /// Stores remaining delay pausing (and eventually, serialization). + /// + [DataField("remainingDelay")] + public TimeSpan? RemainingDelay; public CancellationTokenSource? CancellationTokenSource; public bool ActiveDelay => CancellationTokenSource is { Token: { IsCancellationRequested: false } }; } + + [Serializable, NetSerializable] + public sealed class UseDelayComponentState : ComponentState + { + public readonly TimeSpan LastUseTime; + public readonly TimeSpan Delay; + public readonly TimeSpan? DelayEndTime; + + public UseDelayComponentState(TimeSpan lastUseTime, TimeSpan delay, TimeSpan? delayEndTime) + { + LastUseTime = lastUseTime; + Delay = delay; + DelayEndTime = delayEndTime; + } + } } diff --git a/Content.Shared/Timing/UseDelaySystem.cs b/Content.Shared/Timing/UseDelaySystem.cs new file mode 100644 index 0000000000..b98ea84e00 --- /dev/null +++ b/Content.Shared/Timing/UseDelaySystem.cs @@ -0,0 +1,138 @@ +using System.Threading; +using Content.Shared.Cooldown; +using Robust.Shared.GameStates; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Timing; + +public sealed class UseDelaySystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = default!; + + private HashSet _activeDelays = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + + SubscribeLocalEvent(OnPaused); + } + + private void OnPaused(EntityUid uid, UseDelayComponent component, EntityPausedEvent args) + { + if (args.Paused) + { + // This entity just got paused, but wasn't before + if (component.DelayEndTime != null) + component.RemainingDelay = _gameTiming.CurTime - component.DelayEndTime; + + _activeDelays.Remove(component); + } + else if (component.RemainingDelay == null) + { + // Got unpaused, but had no active delay + return; + } + + // We got unpaused, resume the delay/cooldown. Currently this takes for granted that ItemCooldownComponent + // handles the pausing on its own. I'm not even gonna check, because I CBF fixing it if it doesn't. + component.DelayEndTime = _gameTiming.CurTime + component.RemainingDelay; + Dirty(component); + _activeDelays.Add(component); + } + + private void OnHandleState(EntityUid uid, UseDelayComponent component, ref ComponentHandleState args) + { + if (args.Current is not UseDelayComponentState state) + return; + + component.LastUseTime = state.LastUseTime; + component.Delay = state.Delay; + component.DelayEndTime = state.DelayEndTime; + + if (component.DelayEndTime == null) + _activeDelays.Remove(component); + else + _activeDelays.Add(component); + } + + private void OnGetState(EntityUid uid, UseDelayComponent component, ref ComponentGetState args) + { + args.State = new UseDelayComponentState(component.LastUseTime, component.Delay, component.DelayEndTime); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var toRemove = new RemQueue(); + var curTime = _gameTiming.CurTime; + var mQuery = EntityManager.GetEntityQuery(); + + foreach (var delay in _activeDelays) + { + if (curTime > delay.DelayEndTime + || !mQuery.TryGetComponent(delay.Owner, out var meta) + || meta.Deleted + || delay.CancellationTokenSource?.Token.IsCancellationRequested == true) + { + toRemove.Add(delay); + } + } + + foreach (var delay in toRemove) + { + delay.CancellationTokenSource = null; + delay.DelayEndTime = null; + _activeDelays.Remove(delay); + } + } + + public void BeginDelay(EntityUid uid, UseDelayComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return; + + if (component.ActiveDelay || Deleted(uid)) return; + + component.CancellationTokenSource = new CancellationTokenSource(); + + DebugTools.Assert(!_activeDelays.Contains(component)); + _activeDelays.Add(component); + + var currentTime = _gameTiming.CurTime; + component.LastUseTime = currentTime; + component.DelayEndTime = currentTime + component.Delay; + Dirty(component); + + // TODO just merge these components? + var cooldown = EnsureComp(component.Owner); + cooldown.CooldownStart = currentTime; + cooldown.CooldownEnd = component.DelayEndTime; + } + + public void Cancel(UseDelayComponent component) + { + component.CancellationTokenSource?.Cancel(); + component.CancellationTokenSource = null; + component.DelayEndTime = null; + _activeDelays.Remove(component); + Dirty(component); + + if (TryComp(component.Owner, out var cooldown)) + { + cooldown.CooldownEnd = _gameTiming.CurTime; + } + } + + public void Restart(UseDelayComponent component) + { + component.CancellationTokenSource?.Cancel(); + component.CancellationTokenSource = null; + BeginDelay(component.Owner, component); + } +}