diff --git a/Content.Shared/Actions/Events/ActionDoAfterEvent.cs b/Content.Shared/Actions/Events/ActionDoAfterEvent.cs
new file mode 100644
index 0000000000..3ce2e364f4
--- /dev/null
+++ b/Content.Shared/Actions/Events/ActionDoAfterEvent.cs
@@ -0,0 +1,35 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Actions.Events;
+
+///
+/// The event that triggers when an action doafter is completed or cancelled
+///
+[Serializable, NetSerializable]
+public sealed partial class ActionDoAfterEvent : DoAfterEvent
+{
+ ///
+ /// The action performer
+ ///
+ public readonly NetEntity Performer;
+
+ ///
+ /// The original action use delay, used for repeating actions
+ ///
+ public readonly TimeSpan? OriginalUseDelay;
+
+ ///
+ /// The original request, for validating
+ ///
+ 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
index 0000000000..51e4b6e560
--- /dev/null
+++ b/Content.Shared/Actions/SharedActionsSystem.DoAfter.cs
@@ -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(OnActionDoAfter);
+ }
+
+ private bool TryStartActionDoAfter(Entity ent, Entity 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 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);
+ }
+}
diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs
index c4581cfbff..a8201cbede 100644
--- a/Content.Shared/Actions/SharedActionsSystem.cs
+++ b/Content.Shared/Actions/SharedActionsSystem.cs
@@ -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 _actionQuery;
private EntityQuery _actionsQuery;
@@ -38,6 +40,7 @@ public abstract class SharedActionsSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
+ InitializeActionDoAfter();
_actionQuery = GetEntityQuery();
_actionsQuery = GetEntityQuery();
@@ -256,20 +259,31 @@ public abstract class SharedActionsSystem : EntitySystem
#region Execution
///
/// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it
- /// will raise the relevant
+ /// will raise the relevant action event
///
private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not { } user)
return;
+ TryPerformAction(ev, user);
+ }
+
+ ///
+ ///
+ ///
+ /// The Request Perform Action Event
+ /// The user/performer of the action
+ /// Should this skip the initial doaction request?
+ 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(action, out var actionDoAfterComp) && TryComp(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 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));
diff --git a/Content.Shared/DoAfter/DoAfterArgs.cs b/Content.Shared/DoAfter/DoAfterArgs.cs
index ba2b38ab5d..4452f71829 100644
--- a/Content.Shared/DoAfter/DoAfterArgs.cs
+++ b/Content.Shared/DoAfter/DoAfterArgs.cs
@@ -319,6 +319,7 @@ public enum DuplicateConditions : byte
All = SameTool | SameTarget | SameEvent,
}
+[Serializable, NetSerializable]
public enum AttemptFrequency : byte
{
///
diff --git a/Content.Shared/DoAfter/DoAfterArgsComponent.cs b/Content.Shared/DoAfter/DoAfterArgsComponent.cs
new file mode 100644
index 0000000000..bae1d37983
--- /dev/null
+++ b/Content.Shared/DoAfter/DoAfterArgsComponent.cs
@@ -0,0 +1,116 @@
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.DoAfter;
+
+///
+/// For setting DoAfterArgs on an entity level
+/// Would require some setup, will require a rework eventually
+///
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedDoAfterSystem))]
+public sealed partial class DoAfterArgsComponent : Component
+{
+ #region DoAfterArgsSettings
+ ///
+ ///
+ ///
+ [DataField]
+ public AttemptFrequency AttemptFrequency;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool Broadcast;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public TimeSpan Delay = TimeSpan.FromSeconds(2);
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool Hidden;
+
+ ///
+ /// Should this DoAfter repeat after being completed?
+ ///
+ [DataField]
+ public bool Repeat;
+
+ #region Break/Cancellation Options
+ ///
+ ///
+ ///
+ [DataField]
+ public bool NeedHand;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool BreakOnHandChange = true;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool BreakOnDropItem = true;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool BreakOnMove;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool BreakOnWeightlessMove = true;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public float MovementThreshold = 0.3f;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public float? DistanceThreshold;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool BreakOnDamage;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public FixedPoint2 DamageThreshold = 1;
+
+ ///
+ ///
+ ///
+ [DataField]
+ public bool RequireCanInteract = true;
+ // End Break/Cancellation Options
+ #endregion
+
+ ///
+ /// What should the delay be reduced to after completion?
+ ///
+ [DataField]
+ public TimeSpan? DelayReduction;
+
+ // End DoAfterArgsSettings
+ #endregion
+}
diff --git a/Content.Shared/DoAfter/DoAfterComponent.cs b/Content.Shared/DoAfter/DoAfterComponent.cs
index ce45e35c59..ffe575ebc7 100644
--- a/Content.Shared/DoAfter/DoAfterComponent.cs
+++ b/Content.Shared/DoAfter/DoAfterComponent.cs
@@ -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")]
+ ///
+ /// The id of the next doafter
+ ///
+ [DataField]
public ushort NextId;
- [DataField("doAfters")]
+ ///
+ /// collection of id + doafter
+ ///
+ [DataField]
public Dictionary DoAfters = new();
// Used by obsolete async do afters
diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs
index 1dc1e58be6..c1a3d6ecee 100644
--- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs
+++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs
@@ -29,6 +29,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnDamage);
SubscribeLocalEvent(OnUnpaused);
SubscribeLocalEvent(OnDoAfterGetState);
@@ -313,16 +314,16 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
///
/// Cancels an active DoAfter.
///
- 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);
}
///
/// Cancels an active DoAfter.
///
- 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.
diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml
index 6202b49333..61babbfcea 100644
--- a/Resources/Prototypes/Actions/types.yml
+++ b/Resources/Prototypes/Actions/types.yml
@@ -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