DoAfter support for Actions (#38253)

* 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>
This commit is contained in:
keronshb
2025-09-08 05:55:13 -04:00
committed by GitHub
parent 88e927f10a
commit f885075d2e
8 changed files with 294 additions and 24 deletions

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -5,6 +5,7 @@ using Content.Shared.Actions.Components;
using Content.Shared.Actions.Events; using Content.Shared.Actions.Events;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
@@ -19,7 +20,7 @@ using Robust.Shared.Utility;
namespace Content.Shared.Actions; namespace Content.Shared.Actions;
public abstract class SharedActionsSystem : EntitySystem public abstract partial class SharedActionsSystem : EntitySystem
{ {
[Dependency] protected readonly IGameTiming GameTiming = default!; [Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = 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 SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
private EntityQuery<ActionComponent> _actionQuery; private EntityQuery<ActionComponent> _actionQuery;
private EntityQuery<ActionsComponent> _actionsQuery; private EntityQuery<ActionsComponent> _actionsQuery;
@@ -38,6 +40,7 @@ public abstract class SharedActionsSystem : EntitySystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
InitializeActionDoAfter();
_actionQuery = GetEntityQuery<ActionComponent>(); _actionQuery = GetEntityQuery<ActionComponent>();
_actionsQuery = GetEntityQuery<ActionsComponent>(); _actionsQuery = GetEntityQuery<ActionsComponent>();
@@ -256,20 +259,31 @@ public abstract class SharedActionsSystem : EntitySystem
#region Execution #region Execution
/// <summary> /// <summary>
/// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it /// 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> /// </summary>
private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args) private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args)
{ {
if (args.SenderSession.AttachedEntity is not { } user) if (args.SenderSession.AttachedEntity is not { } user)
return; 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)) if (!_actionsQuery.TryComp(user, out var component))
return; return false;
var actionEnt = GetEntity(ev.Action); var actionEnt = GetEntity(ev.Action);
if (!TryComp(actionEnt, out MetaDataComponent? metaData)) if (!TryComp(actionEnt, out MetaDataComponent? metaData))
return; return false;
var name = Name(actionEnt, metaData); var name = Name(actionEnt, metaData);
@@ -278,26 +292,25 @@ public abstract class SharedActionsSystem : EntitySystem
{ {
_adminLogger.Add(LogType.Action, _adminLogger.Add(LogType.Action,
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {name}."); $"{ToPrettyString(user):user} attempted to perform an action that they do not have: {name}.");
return; return false;
} }
if (GetAction(actionEnt) is not {} action) if (GetAction(actionEnt) is not {} action)
return; return false;
DebugTools.Assert(action.Comp.AttachedEntity == user); DebugTools.Assert(action.Comp.AttachedEntity == user);
if (!action.Comp.Enabled) if (!action.Comp.Enabled)
return; return false;
var curTime = GameTiming.CurTime; var curTime = GameTiming.CurTime;
if (IsCooldownActive(action, curTime)) if (IsCooldownActive(action, curTime))
return; return false;
// check for action use prevention // check for action use prevention
// TODO: make code below use this event with a dedicated component
var attemptEv = new ActionAttemptEvent(user); var attemptEv = new ActionAttemptEvent(user);
RaiseLocalEvent(action, ref attemptEv); RaiseLocalEvent(action, ref attemptEv);
if (attemptEv.Cancelled) if (attemptEv.Cancelled)
return; return false;
// Validate request by checking action blockers and the like // Validate request by checking action blockers and the like
var provider = action.Comp.Container ?? user; var provider = action.Comp.Container ?? user;
@@ -309,10 +322,16 @@ public abstract class SharedActionsSystem : EntitySystem
}; };
RaiseLocalEvent(action, ref validateEv); RaiseLocalEvent(action, ref validateEv);
if (validateEv.Invalid) 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! // All checks passed. Perform the action!
PerformAction((user, component), action); PerformAction((user, component), action);
return true;
} }
private void OnValidate(Entity<ActionComponent> ent, ref ActionValidateEvent args) private void OnValidate(Entity<ActionComponent> ent, ref ActionValidateEvent args)
@@ -530,8 +549,6 @@ public abstract class SharedActionsSystem : EntitySystem
{ {
var handled = false; var handled = false;
var toggledBefore = action.Comp.Toggled;
// Note that attached entity and attached container are allowed to be null here. // Note that attached entity and attached container are allowed to be null here.
if (action.Comp.AttachedEntity != null && action.Comp.AttachedEntity != performer) if (action.Comp.AttachedEntity != null && action.Comp.AttachedEntity != performer)
{ {
@@ -552,6 +569,7 @@ public abstract class SharedActionsSystem : EntitySystem
ev.Performer = performer; ev.Performer = performer;
ev.Action = action; 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)) if (!action.Comp.RaiseOnUser && action.Comp.Container is {} container && !_mindQuery.HasComp(container))
target = container; target = container;
@@ -564,13 +582,12 @@ public abstract class SharedActionsSystem : EntitySystem
if (!handled) if (!handled)
return; // no interaction occurred. return; // no interaction occurred.
// play sound, reduce charges, start cooldown // play sound, start cooldown
if (ev?.Toggle == true) if (ev.Toggle)
SetToggled((action, action), !action.Comp.Toggled); SetToggled((action, action), !action.Comp.Toggled);
_audio.PlayPredicted(action.Comp.Sound, performer, predicted ? performer : null); _audio.PlayPredicted(action.Comp.Sound, performer, predicted ? performer : null);
// TODO: move to ActionCooldown ActionPerformedEvent?
RemoveCooldown((action, action)); RemoveCooldown((action, action));
StartUseDelay((action, action)); StartUseDelay((action, action));

View File

@@ -319,6 +319,7 @@ public enum DuplicateConditions : byte
All = SameTool | SameTarget | SameEvent, All = SameTool | SameTarget | SameEvent,
} }
[Serializable, NetSerializable]
public enum AttemptFrequency : byte public enum AttemptFrequency : byte
{ {
/// <summary> /// <summary>

View File

@@ -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
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -8,10 +9,16 @@ namespace Content.Shared.DoAfter;
[Access(typeof(SharedDoAfterSystem))] [Access(typeof(SharedDoAfterSystem))]
public sealed partial class DoAfterComponent : Component public sealed partial class DoAfterComponent : Component
{ {
[DataField("nextId")] /// <summary>
/// The id of the next doafter
/// </summary>
[DataField]
public ushort NextId; public ushort NextId;
[DataField("doAfters")] /// <summary>
/// collection of id + doafter
/// </summary>
[DataField]
public Dictionary<ushort, DoAfter> DoAfters = new(); public Dictionary<ushort, DoAfter> DoAfters = new();
// Used by obsolete async do afters // Used by obsolete async do afters

View File

@@ -29,6 +29,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<DoAfterComponent, DamageChangedEvent>(OnDamage); SubscribeLocalEvent<DoAfterComponent, DamageChangedEvent>(OnDamage);
SubscribeLocalEvent<DoAfterComponent, EntityUnpausedEvent>(OnUnpaused); SubscribeLocalEvent<DoAfterComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState); SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState);
@@ -313,16 +314,16 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
/// <summary> /// <summary>
/// Cancels an active DoAfter. /// Cancels an active DoAfter.
/// </summary> /// </summary>
public void Cancel(DoAfterId? id, DoAfterComponent? comp = null) public void Cancel(DoAfterId? id, DoAfterComponent? comp = null, bool force = false)
{ {
if (id != null) if (id != null)
Cancel(id.Value.Uid, id.Value.Index, comp); Cancel(id.Value.Uid, id.Value.Index, comp, force);
} }
/// <summary> /// <summary>
/// Cancels an active DoAfter. /// Cancels an active DoAfter.
/// </summary> /// </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)) if (!Resolve(entity, ref comp, false))
return; return;
@@ -333,13 +334,13 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
return; return;
} }
InternalCancel(doAfter, comp); InternalCancel(doAfter, comp, force: force);
Dirty(entity, comp); 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; return;
// Caller is responsible for dirtying the component. // Caller is responsible for dirtying the component.

View File

@@ -7,6 +7,14 @@
components: components:
- type: Action - 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 # an action that is done all in le head and cant be prevented by any means
- type: entity - type: entity
abstract: true abstract: true