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