From b292905216948e0c85b04e8ef65a5291dd868eee Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 25 Apr 2024 22:25:52 -0400 Subject: [PATCH] Expand UseDelay to support multiple delays per entity; fix bible healing and bag pickup (#27234) * Upgraded UseDelay to support multiple delays per entity * Implement secondary delay for bibles. Also some improvements to make it work nicely. * Documentation is good * Reserve the previous change; now Storage uses the special ID and Bible uses the default. * .0 * Added VV support to UseDelayInfo * Serialize better * No register, just setlength --- .../Systems/Hands/HandsUIController.cs | 7 +- .../Fluids/EntitySystems/SpraySystem.cs | 2 +- .../Storage/EntitySystems/StorageSystem.cs | 15 +- .../EntitySystems/SharedStorageSystem.cs | 11 +- Content.Shared/Storage/StorageComponent.cs | 13 ++ Content.Shared/Timing/UseDelayComponent.cs | 53 +++--- Content.Shared/Timing/UseDelaySystem.cs | 154 ++++++++++++++---- 7 files changed, 193 insertions(+), 62 deletions(-) diff --git a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs index 99d7bc77b8..9ee429ba7e 100644 --- a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs +++ b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs @@ -22,6 +22,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered _handsContainers = new(); private readonly Dictionary _handContainerIndices = new(); @@ -450,15 +451,15 @@ public sealed class HandsUIController : UIController, IOnStateEntered(OnStorageFillMapInit); } + protected override void OnMapInit(Entity entity, ref MapInitEvent args) + { + base.OnMapInit(entity, ref args); + + if (TryComp(entity, out var useDelay)) + UseDelay.SetLength((entity, useDelay), entity.Comp.OpenUiCooldown, OpenUiUseDelayID); + } + private void AddUiVerb(EntityUid uid, StorageComponent component, GetVerbsEvent args) { var silent = false; @@ -120,13 +129,13 @@ public sealed partial class StorageSystem : SharedStorageSystem return; // prevent spamming bag open / honkerton honk sound - silent |= TryComp(uid, out var useDelay) && _useDelay.IsDelayed((uid, useDelay)); + silent |= TryComp(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay), OpenUiUseDelayID); if (!silent) { if (!storageComp.IsUiOpen) _audio.PlayPvs(storageComp.StorageOpenSound, uid); if (useDelay != null) - _useDelay.TryResetDelay((uid, useDelay)); + UseDelay.TryResetDelay((uid, useDelay), id: OpenUiUseDelayID); } Log.Debug($"Storage (UID {uid}) \"used\" by player session (UID {player.PlayerSession.AttachedEntity})."); diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index 2021dfe2de..3bec0e1d01 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -72,6 +72,8 @@ public abstract class SharedStorageSystem : EntitySystem private readonly List _sortedSizes = new(); private FrozenDictionary _nextSmallest = FrozenDictionary.Empty; + private const string QuickInsertUseDelayID = "quickInsert"; + protected readonly List CantFillReasons = []; /// @@ -84,6 +86,7 @@ public abstract class SharedStorageSystem : EntitySystem _xformQuery = GetEntityQuery(); _prototype.PrototypesReloaded += OnPrototypesReloaded; + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnStorageGetState); SubscribeLocalEvent(OnStorageHandleState); SubscribeLocalEvent(OnComponentInit, before: new[] { typeof(SharedContainerSystem) }); @@ -118,6 +121,12 @@ public abstract class SharedStorageSystem : EntitySystem UpdatePrototypeCache(); } + protected virtual void OnMapInit(Entity entity, ref MapInitEvent args) + { + if (TryComp(entity, out var useDelayComp)) + UseDelay.SetLength((entity, useDelayComp), entity.Comp.QuickInsertCooldown, QuickInsertUseDelayID); + } + private void OnStorageGetState(EntityUid uid, StorageComponent component, ref ComponentGetState args) { var storedItems = new Dictionary(); @@ -275,7 +284,7 @@ public abstract class SharedStorageSystem : EntitySystem /// private void AfterInteract(EntityUid uid, StorageComponent storageComp, AfterInteractEvent args) { - if (args.Handled || !args.CanReach || !UseDelay.TryResetDelay(uid, checkDelayed: true)) + if (args.Handled || !args.CanReach || !UseDelay.TryResetDelay(uid, checkDelayed: true, id: QuickInsertUseDelayID)) return; // Pick up all entities in a radius around the clicked location. diff --git a/Content.Shared/Storage/StorageComponent.cs b/Content.Shared/Storage/StorageComponent.cs index 16987f1de0..43a93e4b0f 100644 --- a/Content.Shared/Storage/StorageComponent.cs +++ b/Content.Shared/Storage/StorageComponent.cs @@ -57,6 +57,19 @@ namespace Content.Shared.Storage [DataField] public bool QuickInsert; // Can insert storables by clicking them with the storage entity + /// + /// Minimum delay between quick/area insert actions. + /// + /// Used to prevent autoclickers spamming server with individual pickup actions. + public TimeSpan QuickInsertCooldown = TimeSpan.FromSeconds(0.5); + + /// + /// Minimum delay between UI open actions. + /// Used to spamming opening sounds. + /// + [DataField] + public TimeSpan OpenUiCooldown = TimeSpan.Zero; + [DataField] public bool ClickInsert = true; // Can insert stuff by clicking the storage entity with it diff --git a/Content.Shared/Timing/UseDelayComponent.cs b/Content.Shared/Timing/UseDelayComponent.cs index 1560d4dd0b..c7b21bd1fe 100644 --- a/Content.Shared/Timing/UseDelayComponent.cs +++ b/Content.Shared/Timing/UseDelayComponent.cs @@ -1,38 +1,47 @@ using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization; namespace Content.Shared.Timing; /// -/// Timer that creates a cooldown each time an object is activated/used +/// Timer that creates a cooldown each time an object is activated/used. +/// Can support additional, separate cooldown timers on the object by passing a unique ID with the system methods. /// -/// -/// Currently it only supports a single delay per entity, this means that for things that have two delay interactions they will share one timer, so this can cause issues. For example, the bible has a delay when opening the storage UI and when applying it's interaction effect, and they share the same delay. -/// [RegisterComponent] -[NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[NetworkedComponent, AutoGenerateComponentState] [Access(typeof(UseDelaySystem))] public sealed partial class UseDelayComponent : Component { - /// - /// When the delay starts. - /// - [ViewVariables(VVAccess.ReadWrite), DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField] - [AutoPausedField] - public TimeSpan DelayStartTime; + [DataField, AutoNetworkedField] + public Dictionary Delays = []; /// - /// When the delay ends. - /// - [ViewVariables(VVAccess.ReadWrite), DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField] - [AutoPausedField] - public TimeSpan DelayEndTime; - - /// - /// Default delay time + /// Default delay time. /// + /// + /// This is only used at MapInit and should not be expected + /// to reflect the length of the default delay after that. + /// Use instead. + /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] public TimeSpan Delay = TimeSpan.FromSeconds(1); } + +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class UseDelayInfo +{ + [DataField] + public TimeSpan Length { get; set; } + [DataField] + public TimeSpan StartTime { get; set; } + [DataField] + public TimeSpan EndTime { get; set; } + + public UseDelayInfo(TimeSpan length, TimeSpan startTime = default, TimeSpan endTime = default) + { + Length = length; + StartTime = startTime; + EndTime = endTime; + } +} diff --git a/Content.Shared/Timing/UseDelaySystem.cs b/Content.Shared/Timing/UseDelaySystem.cs index 388f31079c..3d2498203c 100644 --- a/Content.Shared/Timing/UseDelaySystem.cs +++ b/Content.Shared/Timing/UseDelaySystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Robust.Shared.Timing; namespace Content.Shared.Timing; @@ -7,53 +8,142 @@ public sealed class UseDelaySystem : EntitySystem [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly MetaDataSystem _metadata = default!; - public void SetDelay(Entity ent, TimeSpan delay) - { - if (ent.Comp.Delay == delay) - return; + private const string DefaultId = "default"; - ent.Comp.Delay = delay; - Dirty(ent); + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnUnpaused); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + // Set default delay length from the prototype + // This makes it easier for simple use cases that only need a single delay + SetLength(ent, ent.Comp.Delay, DefaultId); + } + + private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) + { + // We have to do this manually, since it's not just a single field. + foreach (var entry in ent.Comp.Delays.Values) + { + entry.EndTime += args.PausedTime; + } } /// - /// Returns true if the entity has a currently active UseDelay. + /// Sets the length of the delay with the specified ID. /// - public bool IsDelayed(Entity ent) + public bool SetLength(Entity ent, TimeSpan length, string id = DefaultId) { - return ent.Comp.DelayEndTime >= _gameTiming.CurTime; - } + if (ent.Comp.Delays.TryGetValue(id, out var entry)) + { + if (entry.Length == length) + return true; - /// - /// Cancels the current delay. - /// - public void CancelDelay(Entity ent) - { - ent.Comp.DelayEndTime = _gameTiming.CurTime; - Dirty(ent); - } + entry.Length = length; + } + else + { + ent.Comp.Delays.Add(id, new UseDelayInfo(length)); + } - /// - /// Resets the UseDelay entirely for this entity if possible. - /// - /// Check if the entity has an ongoing delay, return false if it does, return true if it does not. - public bool TryResetDelay(Entity ent, bool checkDelayed = false) - { - if (checkDelayed && IsDelayed(ent)) - return false; - - var curTime = _gameTiming.CurTime; - ent.Comp.DelayStartTime = curTime; - ent.Comp.DelayEndTime = curTime - _metadata.GetPauseTime(ent) + ent.Comp.Delay; Dirty(ent); return true; } - public bool TryResetDelay(EntityUid uid, bool checkDelayed = false, UseDelayComponent? component = null) + /// + /// Returns true if the entity has a currently active UseDelay with the specified ID. + /// + public bool IsDelayed(Entity ent, string id = DefaultId) + { + if (!ent.Comp.Delays.TryGetValue(id, out var entry)) + return false; + + return entry.EndTime >= _gameTiming.CurTime; + } + + /// + /// Cancels the delay with the specified ID. + /// + public void CancelDelay(Entity ent, string id = DefaultId) + { + if (!ent.Comp.Delays.TryGetValue(id, out var entry)) + return; + + entry.EndTime = _gameTiming.CurTime; + Dirty(ent); + } + + /// + /// Tries to get info about the delay with the specified ID. See . + /// + /// + /// + /// + /// + public bool TryGetDelayInfo(Entity ent, [NotNullWhen(true)] out UseDelayInfo? info, string id = DefaultId) + { + return ent.Comp.Delays.TryGetValue(id, out info); + } + + /// + /// Returns info for the delay that will end farthest in the future. + /// + public UseDelayInfo GetLastEndingDelay(Entity ent) + { + var last = ent.Comp.Delays[DefaultId]; + foreach (var entry in ent.Comp.Delays) + { + if (entry.Value.EndTime > last.EndTime) + last = entry.Value; + } + return last; + } + + /// + /// Resets the delay with the specified ID for this entity if possible. + /// + /// Check if the entity has an ongoing delay with the specified ID. + /// If it does, return false and don't reset it. + /// Otherwise reset it and return true. + public bool TryResetDelay(Entity ent, bool checkDelayed = false, string id = DefaultId) + { + if (checkDelayed && IsDelayed(ent, id)) + return false; + + if (!ent.Comp.Delays.TryGetValue(id, out var entry)) + return false; + + var curTime = _gameTiming.CurTime; + entry.StartTime = curTime; + entry.EndTime = curTime - _metadata.GetPauseTime(ent) + entry.Length; + Dirty(ent); + return true; + } + + public bool TryResetDelay(EntityUid uid, bool checkDelayed = false, UseDelayComponent? component = null, string id = DefaultId) { if (!Resolve(uid, ref component, false)) return false; - return TryResetDelay((uid, component), checkDelayed); + return TryResetDelay((uid, component), checkDelayed, id); + } + + /// + /// Resets all delays on the entity. + /// + public void ResetAllDelays(Entity ent) + { + var curTime = _gameTiming.CurTime; + foreach (var entry in ent.Comp.Delays.Values) + { + entry.StartTime = curTime; + entry.EndTime = curTime - _metadata.GetPauseTime(ent) + entry.Length; + } + Dirty(ent); } }