]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
DoAfter support for Actions (#38253)
authorkeronshb <54602815+keronshb@users.noreply.github.com>
Mon, 8 Sep 2025 09:55:13 +0000 (05:55 -0400)
committerGitHub <noreply@github.com>
Mon, 8 Sep 2025 09:55:13 +0000 (12:55 +0300)
* Adds Action DoAfter Events

* Adds DoAfterArgs fields to DoAfterComp

* Adds a base doafter action

* Adds Attempt action doafter logic

* Adds doafter logic to actions

* Changes Action Attempt Doafter and action doafter to take in Performer and the original use delay. Use delay now triggers when a repeated action  is cancelled.

* Readds the TryPerformAction method and readds request perform action into the action doafter events

* Adds a force skip to DoAfter Cancel so we can skip the complete check

* Adds a Delay Reduction field to the comp and to the comp state

* Fixes doafter mispredict, changes doafter comp check to a guard clause, sets delay reduction if it exists.

* Cancels ActionDoAfter if charges is 0

* Serializes Attempt Frequency

* Comment for rework

* Changes todo into a comment

* Moves doafterargs to doafterargscomp

* Adds DoAfterArgs comp to BaseDoAfterAction

* Removes unused trycomp with actionDoAfter

* Replaces DoAfterRepateUseDelay const with timespan.zero

* Removes unused usings

* Makes SharedActionsSystem partial, adds DoAfter partial class to ActionSystem, moves ActionDoAfter logic to the SharedActionsSystem.DoAfter class

* Cleanup and prediction

* Renames OnActionDoAfterAttempt to OnActionDoAfter, moves both to Shared Action DoAfter

* Removes ActionAttemptDoAfterEvent and moves its summaries to ActionDoAfterEvent. Converts OnActionDoAfterAttempt into TryStartActionDoAfter

* Removes Extra check for charges and actiondoafters

* Sloptimization

* Cleanup

* Cleanup

* Adds param descs

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Content.Shared/Actions/Events/ActionDoAfterEvent.cs [new file with mode: 0644]
Content.Shared/Actions/SharedActionsSystem.DoAfter.cs [new file with mode: 0644]
Content.Shared/Actions/SharedActionsSystem.cs
Content.Shared/DoAfter/DoAfterArgs.cs
Content.Shared/DoAfter/DoAfterArgsComponent.cs [new file with mode: 0644]
Content.Shared/DoAfter/DoAfterComponent.cs
Content.Shared/DoAfter/SharedDoAfterSystem.cs
Resources/Prototypes/Actions/types.yml

diff --git a/Content.Shared/Actions/Events/ActionDoAfterEvent.cs b/Content.Shared/Actions/Events/ActionDoAfterEvent.cs
new file mode 100644 (file)
index 0000000..3ce2e36
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Actions.Events;
+
+/// <summary>
+/// The event that triggers when an action doafter is completed or cancelled
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class ActionDoAfterEvent : DoAfterEvent
+{
+    /// <summary>
+    /// The action performer
+    /// </summary>
+    public readonly NetEntity Performer;
+
+    /// <summary>
+    /// The original action use delay, used for repeating actions
+    /// </summary>
+    public readonly TimeSpan? OriginalUseDelay;
+
+    /// <summary>
+    /// The original request, for validating
+    /// </summary>
+    public readonly RequestPerformActionEvent Input;
+
+    public ActionDoAfterEvent(NetEntity performer, TimeSpan? originalUseDelay, RequestPerformActionEvent input)
+    {
+        Performer = performer;
+        OriginalUseDelay = originalUseDelay;
+        Input = input;
+    }
+
+    public override DoAfterEvent Clone() => this;
+}
diff --git a/Content.Shared/Actions/SharedActionsSystem.DoAfter.cs b/Content.Shared/Actions/SharedActionsSystem.DoAfter.cs
new file mode 100644 (file)
index 0000000..51e4b6e
--- /dev/null
@@ -0,0 +1,85 @@
+using Content.Shared.Actions.Events;
+using Content.Shared.DoAfter;
+
+namespace Content.Shared.Actions;
+
+public abstract partial class SharedActionsSystem
+{
+    protected void InitializeActionDoAfter()
+    {
+        SubscribeLocalEvent<DoAfterArgsComponent, ActionDoAfterEvent>(OnActionDoAfter);
+    }
+
+    private bool TryStartActionDoAfter(Entity<DoAfterArgsComponent> ent, Entity<DoAfterComponent?> performer, TimeSpan? originalUseDelay, RequestPerformActionEvent input)
+    {
+        // relay to user
+        if (!Resolve(performer, ref performer.Comp))
+            return false;
+
+        var delay = ent.Comp.Delay;
+
+        var netEnt = GetNetEntity(performer);
+
+        var actionDoAfterEvent = new ActionDoAfterEvent(netEnt, originalUseDelay, input);
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, performer, delay, actionDoAfterEvent, ent.Owner, performer)
+        {
+            AttemptFrequency = ent.Comp.AttemptFrequency,
+            Broadcast = ent.Comp.Broadcast,
+            Hidden = ent.Comp.Hidden,
+            NeedHand = ent.Comp.NeedHand,
+            BreakOnHandChange = ent.Comp.BreakOnHandChange,
+            BreakOnDropItem = ent.Comp.BreakOnDropItem,
+            BreakOnMove = ent.Comp.BreakOnMove,
+            BreakOnWeightlessMove = ent.Comp.BreakOnWeightlessMove,
+            MovementThreshold = ent.Comp.MovementThreshold,
+            DistanceThreshold = ent.Comp.DistanceThreshold,
+            BreakOnDamage = ent.Comp.BreakOnDamage,
+            DamageThreshold = ent.Comp.DamageThreshold,
+            RequireCanInteract = ent.Comp.RequireCanInteract
+        };
+
+        return _doAfter.TryStartDoAfter(doAfterArgs, performer);
+    }
+
+    private void OnActionDoAfter(Entity<DoAfterArgsComponent> ent, ref ActionDoAfterEvent args)
+    {
+        if (!_actionQuery.TryComp(ent, out var actionComp))
+            return;
+
+        var performer = GetEntity(args.Performer);
+        var action = (ent, actionComp);
+
+        // If this doafter is on repeat and was cancelled, start use delay as expected
+        if (args.Cancelled && ent.Comp.Repeat)
+        {
+            SetUseDelay(action, args.OriginalUseDelay);
+            RemoveCooldown(action);
+            StartUseDelay(action);
+            UpdateAction(action);
+            return;
+        }
+
+        args.Repeat = ent.Comp.Repeat;
+
+        // Set the use delay to 0 so this can repeat properly
+        if (ent.Comp.Repeat)
+        {
+            SetUseDelay(action, TimeSpan.Zero);
+        }
+
+        if (args.Cancelled)
+            return;
+
+        // Post original doafter, reduce the time on it now for other casts if ables
+        if (ent.Comp.DelayReduction != null)
+            args.Args.Delay = ent.Comp.DelayReduction.Value;
+
+        // Validate again for charges, blockers, etc
+        if (TryPerformAction(args.Input, performer, skipDoActionRequest: true))
+            return;
+
+        // Cancel this doafter if we can't validate the action
+        _doAfter.Cancel(args.DoAfter.Id, force: true);
+    }
+}
index c4581cfbffb561d31c9b8c216d7f2286e0e09ac4..a8201cbede14c93baa8dcefafc51de648fc86d8e 100644 (file)
@@ -5,6 +5,7 @@ using Content.Shared.Actions.Components;
 using Content.Shared.Actions.Events;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Database;
+using Content.Shared.DoAfter;
 using Content.Shared.Hands;
 using Content.Shared.Interaction;
 using Content.Shared.Inventory.Events;
@@ -19,7 +20,7 @@ using Robust.Shared.Utility;
 
 namespace Content.Shared.Actions;
 
-public abstract class SharedActionsSystem : EntitySystem
+public abstract partial class SharedActionsSystem : EntitySystem
 {
     [Dependency] protected readonly IGameTiming GameTiming = default!;
     [Dependency] private   readonly ISharedAdminLogManager _adminLogger = default!;
@@ -30,6 +31,7 @@ public abstract class SharedActionsSystem : EntitySystem
     [Dependency] private   readonly SharedAudioSystem _audio = default!;
     [Dependency] private   readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private   readonly SharedTransformSystem _transform = default!;
+    [Dependency] private   readonly SharedDoAfterSystem _doAfter = default!;
 
     private EntityQuery<ActionComponent> _actionQuery;
     private EntityQuery<ActionsComponent> _actionsQuery;
@@ -38,6 +40,7 @@ public abstract class SharedActionsSystem : EntitySystem
     public override void Initialize()
     {
         base.Initialize();
+        InitializeActionDoAfter();
 
         _actionQuery = GetEntityQuery<ActionComponent>();
         _actionsQuery = GetEntityQuery<ActionsComponent>();
@@ -256,20 +259,31 @@ public abstract class SharedActionsSystem : EntitySystem
     #region Execution
     /// <summary>
     ///     When receiving a request to perform an action, this validates whether the action is allowed. If it is, it
-    ///     will raise the relevant <see cref="InstantActionEvent"/>
+    ///     will raise the relevant action event
     /// </summary>
     private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args)
     {
         if (args.SenderSession.AttachedEntity is not { } user)
             return;
 
+        TryPerformAction(ev, user);
+    }
+
+    /// <summary>
+    /// <see cref="OnActionRequest"/>
+    /// </summary>
+    /// <param name="ev">The Request Perform Action Event</param>
+    /// <param name="user">The user/performer of the action</param>
+    /// <param name="skipDoActionRequest">Should this skip the initial doaction request?</param>
+    private bool TryPerformAction(RequestPerformActionEvent ev, EntityUid user, bool skipDoActionRequest = false)
+    {
         if (!_actionsQuery.TryComp(user, out var component))
-            return;
+            return false;
 
         var actionEnt = GetEntity(ev.Action);
 
         if (!TryComp(actionEnt, out MetaDataComponent? metaData))
-            return;
+            return false;
 
         var name = Name(actionEnt, metaData);
 
@@ -278,26 +292,25 @@ public abstract class SharedActionsSystem : EntitySystem
         {
             _adminLogger.Add(LogType.Action,
                 $"{ToPrettyString(user):user} attempted to perform an action that they do not have: {name}.");
-            return;
+            return false;
         }
 
         if (GetAction(actionEnt) is not {} action)
-            return;
+            return false;
 
         DebugTools.Assert(action.Comp.AttachedEntity == user);
         if (!action.Comp.Enabled)
-            return;
+            return false;
 
         var curTime = GameTiming.CurTime;
         if (IsCooldownActive(action, curTime))
-            return;
+            return false;
 
         // check for action use prevention
-        // TODO: make code below use this event with a dedicated component
         var attemptEv = new ActionAttemptEvent(user);
         RaiseLocalEvent(action, ref attemptEv);
         if (attemptEv.Cancelled)
-            return;
+            return false;
 
         // Validate request by checking action blockers and the like
         var provider = action.Comp.Container ?? user;
@@ -309,10 +322,16 @@ public abstract class SharedActionsSystem : EntitySystem
         };
         RaiseLocalEvent(action, ref validateEv);
         if (validateEv.Invalid)
-            return;
+            return false;
+
+        if (TryComp<DoAfterArgsComponent>(action, out var actionDoAfterComp) && TryComp<DoAfterComponent>(user, out var performerDoAfterComp) && !skipDoActionRequest)
+        {
+            return TryStartActionDoAfter((action, actionDoAfterComp), (user, performerDoAfterComp), action.Comp.UseDelay, ev);
+        }
 
         // All checks passed. Perform the action!
         PerformAction((user, component), action);
+        return true;
     }
 
     private void OnValidate(Entity<ActionComponent> ent, ref ActionValidateEvent args)
@@ -530,8 +549,6 @@ public abstract class SharedActionsSystem : EntitySystem
     {
         var handled = false;
 
-        var toggledBefore = action.Comp.Toggled;
-
         // Note that attached entity and attached container are allowed to be null here.
         if (action.Comp.AttachedEntity != null && action.Comp.AttachedEntity != performer)
         {
@@ -552,6 +569,7 @@ public abstract class SharedActionsSystem : EntitySystem
         ev.Performer = performer;
         ev.Action = action;
 
+        // TODO: This is where we'd add support for event lists
         if (!action.Comp.RaiseOnUser && action.Comp.Container is {} container && !_mindQuery.HasComp(container))
             target = container;
 
@@ -564,13 +582,12 @@ public abstract class SharedActionsSystem : EntitySystem
         if (!handled)
             return; // no interaction occurred.
 
-        // play sound, reduce charges, start cooldown
-        if (ev?.Toggle == true)
+        // play sound, start cooldown
+        if (ev.Toggle)
             SetToggled((action, action), !action.Comp.Toggled);
 
         _audio.PlayPredicted(action.Comp.Sound, performer, predicted ? performer : null);
 
-        // TODO: move to ActionCooldown ActionPerformedEvent?
         RemoveCooldown((action, action));
         StartUseDelay((action, action));
 
index ba2b38ab5d85d751e4d8a6f9b7e80048a63d7b45..4452f71829b2ac8f9ef44aadb6e6e34b471b8d77 100644 (file)
@@ -319,6 +319,7 @@ public enum DuplicateConditions : byte
     All = SameTool | SameTarget | SameEvent,
 }
 
+[Serializable, NetSerializable]
 public enum AttemptFrequency : byte
 {
     /// <summary>
diff --git a/Content.Shared/DoAfter/DoAfterArgsComponent.cs b/Content.Shared/DoAfter/DoAfterArgsComponent.cs
new file mode 100644 (file)
index 0000000..bae1d37
--- /dev/null
@@ -0,0 +1,116 @@
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.DoAfter;
+
+/// <summary>
+/// For setting DoAfterArgs on an entity level
+/// Would require some setup, will require a rework eventually
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedDoAfterSystem))]
+public sealed partial class DoAfterArgsComponent : Component
+{
+    #region DoAfterArgsSettings
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.AttemptFrequency"/>
+    /// </summary>
+    [DataField]
+    public AttemptFrequency AttemptFrequency;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.Broadcast"/>
+    /// </summary>
+    [DataField]
+    public bool Broadcast;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.Delay"/>
+    /// </summary>
+    [DataField]
+    public TimeSpan Delay = TimeSpan.FromSeconds(2);
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.Hidden"/>
+    /// </summary>
+    [DataField]
+    public bool Hidden;
+
+    /// <summary>
+    /// Should this DoAfter repeat after being completed?
+    /// </summary>
+    [DataField]
+    public bool Repeat;
+
+    #region Break/Cancellation Options
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.NeedHand"/>
+    /// </summary>
+    [DataField]
+    public bool NeedHand;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.BreakOnHandChange"/>
+    /// </summary>
+    [DataField]
+    public bool BreakOnHandChange = true;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.BreakOnDropItem"/>
+    /// </summary>
+    [DataField]
+    public bool BreakOnDropItem = true;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.BreakOnMove"/>
+    /// </summary>
+    [DataField]
+    public bool BreakOnMove;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.BreakOnWeightlessMove"/>
+    /// </summary>
+    [DataField]
+    public bool BreakOnWeightlessMove = true;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.MovementThreshold"/>
+    /// </summary>
+    [DataField]
+    public float MovementThreshold = 0.3f;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.DistanceThreshold"/>
+    /// </summary>
+    [DataField]
+    public float? DistanceThreshold;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.BreakOnDamage"/>
+    /// </summary>
+    [DataField]
+    public bool BreakOnDamage;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.DamageThreshold"/>
+    /// </summary>
+    [DataField]
+    public FixedPoint2 DamageThreshold = 1;
+
+    /// <summary>
+    /// <inheritdoc cref="DoAfterArgs.RequireCanInteract"/>
+    /// </summary>
+    [DataField]
+    public bool RequireCanInteract = true;
+    // End Break/Cancellation Options
+    #endregion
+
+    /// <summary>
+    /// What should the delay be reduced to after completion?
+    /// </summary>
+    [DataField]
+    public TimeSpan? DelayReduction;
+
+    // End DoAfterArgsSettings
+    #endregion
+}
index ce45e35c592c7939db249f404922fc245ad6704e..ffe575ebc74d2fbd560b1ffa959e56b821d0d94a 100644 (file)
@@ -1,4 +1,5 @@
 using System.Threading.Tasks;
+using Content.Shared.FixedPoint;
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
 
@@ -8,10 +9,16 @@ namespace Content.Shared.DoAfter;
 [Access(typeof(SharedDoAfterSystem))]
 public sealed partial class DoAfterComponent : Component
 {
-    [DataField("nextId")]
+    /// <summary>
+    /// The id of the next doafter
+    /// </summary>
+    [DataField]
     public ushort NextId;
 
-    [DataField("doAfters")]
+    /// <summary>
+    /// collection of id + doafter
+    /// </summary>
+    [DataField]
     public Dictionary<ushort, DoAfter> DoAfters = new();
 
     // Used by obsolete async do afters
index 1dc1e58be6472b6a3ab4b290caa8ad4114c39b26..c1a3d6ecee4800d298ef969dbdf055f69f8b0942 100644 (file)
@@ -29,6 +29,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
     public override void Initialize()
     {
         base.Initialize();
+
         SubscribeLocalEvent<DoAfterComponent, DamageChangedEvent>(OnDamage);
         SubscribeLocalEvent<DoAfterComponent, EntityUnpausedEvent>(OnUnpaused);
         SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState);
@@ -313,16 +314,16 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
     /// <summary>
     ///     Cancels an active DoAfter.
     /// </summary>
-    public void Cancel(DoAfterId? id, DoAfterComponent? comp = null)
+    public void Cancel(DoAfterId? id, DoAfterComponent? comp = null, bool force = false)
     {
         if (id != null)
-            Cancel(id.Value.Uid, id.Value.Index, comp);
+            Cancel(id.Value.Uid, id.Value.Index, comp, force);
     }
 
     /// <summary>
     ///     Cancels an active DoAfter.
     /// </summary>
-    public void Cancel(EntityUid entity, ushort id, DoAfterComponent? comp = null)
+    public void Cancel(EntityUid entity, ushort id, DoAfterComponent? comp = null, bool force = false)
     {
         if (!Resolve(entity, ref comp, false))
             return;
@@ -333,13 +334,13 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
             return;
         }
 
-        InternalCancel(doAfter, comp);
+        InternalCancel(doAfter, comp, force: force);
         Dirty(entity, comp);
     }
 
-    private void InternalCancel(DoAfter doAfter, DoAfterComponent component)
+    private void InternalCancel(DoAfter doAfter, DoAfterComponent component, bool force = false)
     {
-        if (doAfter.Cancelled || doAfter.Completed)
+        if (doAfter.Cancelled || (doAfter.Completed && !force))
             return;
 
         // Caller is responsible for dirtying the component.
index 6202b49333538bc12852c8616e90b51e1998439c..61babbfcea7a0ca1ef0bd0a68e09971476534e68 100644 (file)
@@ -7,6 +7,14 @@
   components:
   - type: Action
 
+# base proto for an action that requires a DoAfter
+- type: entity
+  abstract: true
+  parent: BaseAction
+  id: BaseDoAfterAction
+  components:
+  - type: DoAfterArgs
+
 # an action that is done all in le head and cant be prevented by any means
 - type: entity
   abstract: true