]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Action charges refactor (#33993)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Fri, 18 Apr 2025 03:45:48 +0000 (13:45 +1000)
committerGitHub <noreply@github.com>
Fri, 18 Apr 2025 03:45:48 +0000 (13:45 +1000)
* Action charges refactor

- Fixes the slight godmoding of baseactioncomponent.
- Gets back 1ms of server time.

* chorg

* Remove FrameUpdate

* Fixes

* More fixes

* Combine

* Fixes

* Updates

* weh

* Last fixes

* weh

* Fix naughty

* YAML fixes

* This one too

* Merge conflicts

* This thing

* Review

* Fix this as well

* Icon fix

* weh

* Review

* Review

* seamless

* Review

30 files changed:
Content.Client/Actions/ActionsSystem.cs
Content.Client/Charges/ChargesSystem.cs [new file with mode: 0644]
Content.Client/Charges/Systems/ChargesSystem.cs [deleted file]
Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
Content.Server/Charges/ChargesSystem.cs [new file with mode: 0644]
Content.Server/Charges/Components/AutoRechargeComponent.cs [deleted file]
Content.Server/Charges/Systems/ChargesSystem.cs [deleted file]
Content.Server/Flash/FlashSystem.cs
Content.Shared/Actions/BaseActionComponent.cs
Content.Shared/Actions/SharedActionsSystem.cs
Content.Shared/Charges/Components/AutoRechargeComponent.cs [new file with mode: 0644]
Content.Shared/Charges/Components/LimitedChargesComponent.cs
Content.Shared/Charges/Systems/SharedChargesSystem.cs
Content.Shared/Emag/Systems/EmagSystem.cs
Content.Shared/Magic/SpellbookSystem.cs
Content.Shared/Ninja/Systems/DashAbilitySystem.cs
Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs
Content.Shared/RCD/Components/RCDAmmoComponent.cs
Content.Shared/RCD/Systems/RCDAmmoSystem.cs
Content.Shared/RCD/Systems/RCDSystem.cs
Resources/Prototypes/Actions/types.yml
Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
Resources/Prototypes/Entities/Objects/Tools/lantern.yml
Resources/Prototypes/Entities/Objects/Tools/tools.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
Resources/Prototypes/Entities/Objects/Weapons/security.yml
Resources/Prototypes/Magic/animate_spell.yml
Resources/Prototypes/Magic/projectile_spells.yml
Resources/Prototypes/Magic/staves.yml

index 5f0a8e1f2f408544863be0cb554182d3190e2f38..0302739816de79a7203e14fed223164821b5a973 100644 (file)
@@ -1,6 +1,7 @@
 using System.IO;
 using System.Linq;
 using Content.Shared.Actions;
+using Content.Shared.Charges.Systems;
 using JetBrains.Annotations;
 using Robust.Client.Player;
 using Robust.Shared.ContentPack;
@@ -22,6 +23,7 @@ namespace Content.Client.Actions
     {
         public delegate void OnActionReplaced(EntityUid actionId);
 
+        [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
         [Dependency] private readonly IPlayerManager _playerManager = default!;
         [Dependency] private readonly IResourceManager _resources = default!;
         [Dependency] private readonly ISerializationManager _serialization = default!;
@@ -51,29 +53,6 @@ namespace Content.Client.Actions
             SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
         }
 
-        public override void FrameUpdate(float frameTime)
-        {
-            base.FrameUpdate(frameTime);
-
-            var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
-            while (worldActionQuery.MoveNext(out var uid, out var action))
-            {
-                UpdateAction(uid, action);
-            }
-
-            var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
-            while (instantActionQuery.MoveNext(out var uid, out var action))
-            {
-                UpdateAction(uid, action);
-            }
-
-            var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
-            while (entityActionQuery.MoveNext(out var uid, out var action))
-            {
-                UpdateAction(uid, action);
-            }
-        }
-
         private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
         {
             if (args.Current is not InstantActionComponentState state)
@@ -127,9 +106,6 @@ namespace Content.Client.Actions
             component.Toggled = state.Toggled;
             component.Cooldown = state.Cooldown;
             component.UseDelay = state.UseDelay;
-            component.Charges = state.Charges;
-            component.MaxCharges = state.MaxCharges;
-            component.RenewCharges = state.RenewCharges;
             component.Container = EnsureEntity<T>(state.Container, uid);
             component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
             component.CheckCanInteract = state.CheckCanInteract;
@@ -152,7 +128,8 @@ namespace Content.Client.Actions
             if (!ResolveActionData(actionId, ref action))
                 return;
 
-            action.IconColor = action.Charges < 1 ? action.DisabledIconColor : action.OriginalIconColor;
+            // TODO: Decouple this.
+            action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor;
 
             base.UpdateAction(actionId, action);
             if (_playerManager.LocalEntity != action.AttachedEntity)
diff --git a/Content.Client/Charges/ChargesSystem.cs b/Content.Client/Charges/ChargesSystem.cs
new file mode 100644 (file)
index 0000000..2c7e053
--- /dev/null
@@ -0,0 +1,52 @@
+using Content.Client.Actions;
+using Content.Shared.Actions;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
+
+namespace Content.Client.Charges;
+
+public sealed class ChargesSystem : SharedChargesSystem
+{
+    [Dependency] private readonly ActionsSystem _actions = default!;
+
+    private Dictionary<EntityUid, int> _lastCharges = new();
+    private Dictionary<EntityUid, int> _tempLastCharges = new();
+
+    public override void Update(float frameTime)
+    {
+        // Technically this should probably be in frameupdate but no one will ever notice a tick of delay on this.
+        base.Update(frameTime);
+
+        if (!_timing.IsFirstTimePredicted)
+            return;
+
+        // Update recharging actions. Server doesn't actually care about this and it's a waste of performance, actions are immediate.
+        var query = AllEntityQuery<AutoRechargeComponent, LimitedChargesComponent>();
+
+        while (query.MoveNext(out var uid, out var recharge, out var charges))
+        {
+            BaseActionComponent? actionComp = null;
+
+            if (!_actions.ResolveActionData(uid, ref actionComp, logError: false))
+                continue;
+
+            var current = GetCurrentCharges((uid, charges, recharge));
+
+            if (!_lastCharges.TryGetValue(uid, out var last) || current != last)
+            {
+                _actions.UpdateAction(uid, actionComp);
+            }
+
+            _tempLastCharges[uid] = current;
+        }
+
+        _lastCharges.Clear();
+
+        foreach (var (uid, value) in _tempLastCharges)
+        {
+            _lastCharges[uid] = value;
+        }
+
+        _tempLastCharges.Clear();
+    }
+}
diff --git a/Content.Client/Charges/Systems/ChargesSystem.cs b/Content.Client/Charges/Systems/ChargesSystem.cs
deleted file mode 100644 (file)
index 9170ac5..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared.Charges.Systems;
-
-namespace Content.Client.Charges.Systems;
-
-public sealed class ChargesSystem : SharedChargesSystem { }
index d020a7135955f5592456761608ee87b11f6f1190..a4157517ce04760094b680eea71b87b2bbda07e2 100644 (file)
@@ -12,6 +12,7 @@ using Content.Client.UserInterface.Systems.Actions.Widgets;
 using Content.Client.UserInterface.Systems.Actions.Windows;
 using Content.Client.UserInterface.Systems.Gameplay;
 using Content.Shared.Actions;
+using Content.Shared.Charges.Systems;
 using Content.Shared.Input;
 using Robust.Client.GameObjects;
 using Robust.Client.Graphics;
@@ -42,9 +43,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
     [Dependency] private readonly IOverlayManager _overlays = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly IPlayerManager _playerManager = default!;
-    [Dependency] private readonly IEntityManager _entMan = default!;
     [Dependency] private readonly IInputManager _input = default!;
 
+    [UISystemDependency] private readonly SharedChargesSystem _sharedCharges = default!;
     [UISystemDependency] private readonly ActionsSystem? _actionsSystem = default;
     [UISystemDependency] private readonly InteractionOutlineSystem? _interactionOutline = default;
     [UISystemDependency] private readonly TargetOutlineSystem? _targetOutline = default;
@@ -173,7 +174,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
 
         // Is the action currently valid?
         if (!action.Enabled
-            || action is { Charges: 0, RenewCharges: false }
             || action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
         {
             // The user is targeting with this action, but it is not valid. Maybe mark this click as
@@ -483,7 +483,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
                 continue;
             }
 
-            var button = new ActionButton(_entMan, _spriteSystem, this) {Locked = true};
+            var button = new ActionButton(EntityManager, _spriteSystem, this) {Locked = true};
             button.ActionPressed += OnWindowActionPressed;
             button.ActionUnpressed += OnWindowActionUnPressed;
             button.ActionFocusExited += OnWindowActionFocusExisted;
index 0d12d8717111197cf141de3d5da7005b1c454fd7..9e7ef6be4d652520f87416690de68d41f84966c5 100644 (file)
@@ -4,6 +4,8 @@ using Content.Client.Actions.UI;
 using Content.Client.Cooldown;
 using Content.Client.Stylesheets;
 using Content.Shared.Actions;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
 using Robust.Client.GameObjects;
 using Robust.Client.Graphics;
 using Robust.Client.UserInterface;
@@ -22,10 +24,13 @@ public sealed class ActionButton : Control, IEntityControl
     private IEntityManager _entities;
     private SpriteSystem? _spriteSys;
     private ActionUIController? _controller;
+    private SharedChargesSystem _sharedChargesSys;
     private bool _beingHovered;
     private bool _depressed;
     private bool _toggled;
 
+    private int _lastCharges;
+
     public BoundKeyFunction? KeyBind
     {
         set
@@ -65,6 +70,7 @@ public sealed class ActionButton : Control, IEntityControl
 
         _entities = entities;
         _spriteSys = spriteSys;
+        _sharedChargesSys = _entities.System<SharedChargesSystem>();
         _controller = controller;
 
         MouseFilter = MouseFilterMode.Pass;
@@ -194,14 +200,22 @@ public sealed class ActionButton : Control, IEntityControl
 
         var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
         var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
+        FormattedMessage? chargesText = null;
 
-        if (_action is { Charges: not null })
+        // TODO: Don't touch this use an event make callers able to add their own shit for actions or I kill you.
+        if (_entities.TryGetComponent(ActionId, out LimitedChargesComponent? actionCharges))
         {
-            var charges = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {_action.Charges.Value.ToString()}/{_action.MaxCharges.ToString()}"));
-            return new ActionAlertTooltip(name, decr, charges: charges);
+            var charges = _sharedChargesSys.GetCurrentCharges((ActionId.Value, actionCharges, null));
+            chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
+
+            if (_entities.TryGetComponent(ActionId, out AutoRechargeComponent? autoRecharge))
+            {
+                var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((ActionId.Value, actionCharges, autoRecharge));
+                chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
+            }
         }
 
-        return new ActionAlertTooltip(name, decr);
+        return new ActionAlertTooltip(name, decr, charges: chargesText);
     }
 
     protected override void ControlFocusExited()
diff --git a/Content.Server/Charges/ChargesSystem.cs b/Content.Server/Charges/ChargesSystem.cs
new file mode 100644 (file)
index 0000000..6883dcb
--- /dev/null
@@ -0,0 +1,8 @@
+using Content.Shared.Charges.Systems;
+
+namespace Content.Server.Charges;
+
+public sealed class ChargesSystem : SharedChargesSystem
+{
+
+}
diff --git a/Content.Server/Charges/Components/AutoRechargeComponent.cs b/Content.Server/Charges/Components/AutoRechargeComponent.cs
deleted file mode 100644 (file)
index 165b181..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-using Content.Server.Charges.Systems;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Server.Charges.Components;
-
-/// <summary>
-/// Something with limited charges that can be recharged automatically.
-/// Requires LimitedChargesComponent to function.
-/// </summary>
-// TODO: no reason this cant be predicted and server system deleted
-[RegisterComponent, AutoGenerateComponentPause]
-[Access(typeof(ChargesSystem))]
-public sealed partial class AutoRechargeComponent : Component
-{
-    /// <summary>
-    /// The time it takes to regain a single charge
-    /// </summary>
-    [DataField("rechargeDuration"), ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
-
-    /// <summary>
-    /// The time when the next charge will be added
-    /// </summary>
-    [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
-    [AutoPausedField]
-    public TimeSpan NextChargeTime;
-}
diff --git a/Content.Server/Charges/Systems/ChargesSystem.cs b/Content.Server/Charges/Systems/ChargesSystem.cs
deleted file mode 100644 (file)
index 974928e..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-using Content.Server.Charges.Components;
-using Content.Shared.Charges.Components;
-using Content.Shared.Charges.Systems;
-using Content.Shared.Examine;
-using Robust.Shared.Timing;
-
-namespace Content.Server.Charges.Systems;
-
-public sealed class ChargesSystem : SharedChargesSystem
-{
-    [Dependency] private readonly IGameTiming _timing = default!;
-
-    public override void Update(float frameTime)
-    {
-        base.Update(frameTime);
-
-        var query = EntityQueryEnumerator<LimitedChargesComponent, AutoRechargeComponent>();
-        while (query.MoveNext(out var uid, out var charges, out var recharge))
-        {
-            if (charges.Charges == charges.MaxCharges || _timing.CurTime < recharge.NextChargeTime)
-                continue;
-
-            AddCharges(uid, 1, charges);
-            recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
-        }
-    }
-
-    protected override void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
-    {
-        base.OnExamine(uid, comp, args);
-
-        // only show the recharging info if it's not full
-        if (!args.IsInDetailsRange || comp.Charges == comp.MaxCharges || !TryComp<AutoRechargeComponent>(uid, out var recharge))
-            return;
-
-        var timeRemaining = Math.Round((recharge.NextChargeTime - _timing.CurTime).TotalSeconds);
-        args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining)));
-    }
-
-    public override void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
-    {
-        if (!Query.Resolve(uid, ref comp, false))
-            return;
-
-        var startRecharge = comp.Charges == comp.MaxCharges;
-        base.AddCharges(uid, change, comp);
-
-        // if a charge was just used from full, start the recharge timer
-        // TODO: probably make this an event instead of having le server system that just does this
-        if (change < 0 && startRecharge && TryComp<AutoRechargeComponent>(uid, out var recharge))
-            recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
-    }
-}
index 60c09efaeab2d34f318b01445eb68244c17134e3..f904678821f13c3e6383526bc19ce791b0414fc2 100644 (file)
@@ -29,7 +29,7 @@ namespace Content.Server.Flash
     {
         [Dependency] private readonly AppearanceSystem _appearance = default!;
         [Dependency] private readonly AudioSystem _audio = default!;
-        [Dependency] private readonly SharedChargesSystem _charges = default!;
+        [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
         [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
         [Dependency] private readonly SharedTransformSystem _transform = default!;
         [Dependency] private readonly ExamineSystemShared _examine = default!;
@@ -86,15 +86,15 @@ namespace Content.Server.Flash
                 return false;
 
             TryComp<LimitedChargesComponent>(uid, out var charges);
-            if (_charges.IsEmpty(uid, charges))
+            if (_sharedCharges.IsEmpty((uid, charges)))
                 return false;
 
-            _charges.UseCharge(uid, charges);
+            _sharedCharges.TryUseCharge((uid, charges));
             _audio.PlayPvs(comp.Sound, uid);
             comp.Flashing = true;
             _appearance.SetData(uid, FlashVisuals.Flashing, true);
 
-            if (_charges.IsEmpty(uid, charges))
+            if (_sharedCharges.IsEmpty((uid, charges)))
             {
                 _appearance.SetData(uid, FlashVisuals.Burnt, true);
                 _tag.AddTag(uid, TrashTag);
index 25b36df2afe986d0e83b46e50f8e0a5be2d8fb42..05abe30f24edffb00757eb55a91d28ddb5dcd222 100644 (file)
@@ -86,24 +86,6 @@ public abstract partial class BaseActionComponent : Component
     /// </summary>
     [DataField("useDelay")] public TimeSpan? UseDelay;
 
-    /// <summary>
-    ///     Convenience tool for actions with limited number of charges. Automatically decremented on use, and the
-    ///     action is disabled when it reaches zero. Does NOT automatically remove the action from the action bar.
-    ///     However, charges will regenerate if <see cref="RenewCharges"/> is enabled and the action will not disable
-    ///     when charges reach zero.
-    /// </summary>
-    [DataField("charges")] public int? Charges;
-
-    /// <summary>
-    ///     The max charges this action has. If null, this is set automatically from <see cref="Charges"/> on mapinit.
-    /// </summary>
-    [DataField] public int? MaxCharges;
-
-    /// <summary>
-    ///     If enabled, charges will regenerate after a <see cref="Cooldown"/> is complete
-    /// </summary>
-    [DataField("renewCharges")]public bool RenewCharges;
-
     /// <summary>
     /// The entity that contains this action. If the action is innate, this may be the user themselves.
     /// This should almost always be non-null.
@@ -209,9 +191,6 @@ public abstract class BaseActionComponentState : ComponentState
     public bool Toggled;
     public (TimeSpan Start, TimeSpan End)? Cooldown;
     public TimeSpan? UseDelay;
-    public int? Charges;
-    public int? MaxCharges;
-    public bool RenewCharges;
     public NetEntity? Container;
     public NetEntity? EntityIcon;
     public bool CheckCanInteract;
@@ -243,9 +222,6 @@ public abstract class BaseActionComponentState : ComponentState
         Toggled = component.Toggled;
         Cooldown = component.Cooldown;
         UseDelay = component.UseDelay;
-        Charges = component.Charges;
-        MaxCharges = component.MaxCharges;
-        RenewCharges = component.RenewCharges;
         CheckCanInteract = component.CheckCanInteract;
         CheckConsciousness = component.CheckConsciousness;
         ClientExclusive = component.ClientExclusive;
index 30f1af846568ed22d01975e35a476e2e37a3c4e6..e4aa44cf54b2b247e4736ae3b102943cb4302c7c 100644 (file)
@@ -21,14 +21,14 @@ namespace Content.Shared.Actions;
 public abstract class SharedActionsSystem : EntitySystem
 {
     [Dependency] protected readonly IGameTiming GameTiming = default!;
-    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
-    [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
-    [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
-    [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
-    [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+    [Dependency] private   readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private   readonly ActionBlockerSystem _actionBlockerSystem = default!;
+    [Dependency] private   readonly ActionContainerSystem _actionContainer = default!;
+    [Dependency] private   readonly EntityWhitelistSystem _whitelistSystem = default!;
+    [Dependency] private   readonly RotateToFaceSystem _rotateToFaceSystem = default!;
+    [Dependency] private   readonly SharedAudioSystem _audio = default!;
+    [Dependency] private   readonly SharedInteractionSystem _interactionSystem = default!;
+    [Dependency] private   readonly SharedTransformSystem _transformSystem = default!;
 
     public override void Initialize()
     {
@@ -69,47 +69,9 @@ public abstract class SharedActionsSystem : EntitySystem
         SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
     }
 
-    public override void Update(float frameTime)
-    {
-        base.Update(frameTime);
-
-        var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
-        while (worldActionQuery.MoveNext(out var uid, out var action))
-        {
-            if (IsCooldownActive(action) || !ShouldResetCharges(action))
-                continue;
-
-            ResetCharges(uid, dirty: true);
-        }
-
-        var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
-        while (instantActionQuery.MoveNext(out var uid, out var action))
-        {
-            if (IsCooldownActive(action) || !ShouldResetCharges(action))
-                continue;
-
-            ResetCharges(uid, dirty: true);
-        }
-
-        var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
-        while (entityActionQuery.MoveNext(out var uid, out var action))
-        {
-            if (IsCooldownActive(action) || !ShouldResetCharges(action))
-                continue;
-
-            ResetCharges(uid, dirty: true);
-        }
-    }
-
     private void OnActionMapInit(EntityUid uid, BaseActionComponent component, MapInitEvent args)
     {
         component.OriginalIconColor = component.IconColor;
-
-        if (component.Charges == null)
-            return;
-
-        component.MaxCharges ??= component.Charges.Value;
-        Dirty(uid, component);
     }
 
     private void OnActionShutdown(EntityUid uid, BaseActionComponent component, ComponentShutdown args)
@@ -312,68 +274,6 @@ public abstract class SharedActionsSystem : EntitySystem
         Dirty(actionId.Value, action);
     }
 
-    public void SetCharges(EntityUid? actionId, int? charges)
-    {
-        if (!TryGetActionData(actionId, out var action) ||
-            action.Charges == charges)
-        {
-            return;
-        }
-
-        action.Charges = charges;
-        UpdateAction(actionId, action);
-        Dirty(actionId.Value, action);
-    }
-
-    public int? GetCharges(EntityUid? actionId)
-    {
-        if (!TryGetActionData(actionId, out var action))
-            return null;
-
-        return action.Charges;
-    }
-
-    public void AddCharges(EntityUid? actionId, int addCharges)
-    {
-        if (!TryGetActionData(actionId, out var action) || action.Charges == null || addCharges < 1)
-            return;
-
-        action.Charges += addCharges;
-        UpdateAction(actionId, action);
-        Dirty(actionId.Value, action);
-    }
-
-    public void RemoveCharges(EntityUid? actionId, int? removeCharges)
-    {
-        if (!TryGetActionData(actionId, out var action) || action.Charges == null)
-            return;
-
-        if (removeCharges == null)
-            action.Charges = removeCharges;
-        else
-            action.Charges -= removeCharges;
-
-        if (action.Charges is < 0)
-            action.Charges = null;
-
-        UpdateAction(actionId, action);
-        Dirty(actionId.Value, action);
-    }
-
-    public void ResetCharges(EntityUid? actionId, bool update = false, bool dirty = false)
-    {
-        if (!TryGetActionData(actionId, out var action))
-            return;
-
-        action.Charges = action.MaxCharges;
-
-        if (update)
-            UpdateAction(actionId, action);
-
-        if (dirty)
-            Dirty(actionId.Value, action);
-    }
-
     private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
     {
         args.State = new ActionsComponentState(GetNetEntitySet(component.Actions));
@@ -416,6 +316,10 @@ public abstract class SharedActionsSystem : EntitySystem
         if (!action.Enabled)
             return;
 
+        var curTime = GameTiming.CurTime;
+        if (IsCooldownActive(action, curTime))
+            return;
+
         // check for action use prevention
         // TODO: make code below use this event with a dedicated component
         var attemptEv = new ActionAttemptEvent(user);
@@ -423,14 +327,6 @@ public abstract class SharedActionsSystem : EntitySystem
         if (attemptEv.Cancelled)
             return;
 
-        var curTime = GameTiming.CurTime;
-        if (IsCooldownActive(action, curTime))
-            return;
-
-        // TODO: Replace with individual charge recovery when we have the visuals to aid it
-        if (action is { Charges: < 1, RenewCharges: true })
-            ResetCharges(actionEnt, true, true);
-
         BaseActionEvent? performEvent = null;
 
         if (action.CheckConsciousness && !_actionBlockerSystem.CanConsciouslyPerformAction(user))
@@ -705,16 +601,8 @@ public abstract class SharedActionsSystem : EntitySystem
 
         var dirty = toggledBefore != action.Toggled;
 
-        if (action.Charges != null)
-        {
-            dirty = true;
-            action.Charges--;
-            if (action is { Charges: 0, RenewCharges: false })
-                action.Enabled = false;
-        }
-
         action.Cooldown = null;
-        if (action is { UseDelay: not null, Charges: null or < 1 })
+        if (action is { UseDelay: not null})
         {
             dirty = true;
             action.Cooldown = (curTime, curTime + action.UseDelay.Value);
@@ -1014,8 +902,6 @@ public abstract class SharedActionsSystem : EntitySystem
         if (!action.Enabled)
             return false;
 
-        if (action.Charges.HasValue && action.Charges <= 0)
-            return false;
 
         var curTime = GameTiming.CurTime;
         if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
@@ -1125,15 +1011,9 @@ public abstract class SharedActionsSystem : EntitySystem
     /// <summary>
     ///     Checks if the action has a cooldown and if it's still active
     /// </summary>
-    protected bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
+    public bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
     {
-        curTime ??= GameTiming.CurTime;
         // TODO: Check for charge recovery timer
         return action.Cooldown.HasValue && action.Cooldown.Value.End > curTime;
     }
-
-    protected bool ShouldResetCharges(BaseActionComponent action)
-    {
-        return action is { Charges: < 1, RenewCharges: true };
-    }
 }
diff --git a/Content.Shared/Charges/Components/AutoRechargeComponent.cs b/Content.Shared/Charges/Components/AutoRechargeComponent.cs
new file mode 100644 (file)
index 0000000..7047830
--- /dev/null
@@ -0,0 +1,19 @@
+using Content.Shared.Charges.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Charges.Components;
+
+/// <summary>
+/// Something with limited charges that can be recharged automatically.
+/// Requires LimitedChargesComponent to function.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedChargesSystem))]
+public sealed partial class AutoRechargeComponent : Component
+{
+    /// <summary>
+    /// The time it takes to regain a single charge
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
+}
index 6973ffbe72ec58a67f8e3454616fbd35332753b0..ff926fc158eded4e965a0aecd2693f0e6c6d8bf1 100644 (file)
@@ -1,24 +1,27 @@
 using Content.Shared.Charges.Systems;
 using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Charges.Components;
 
-[RegisterComponent, NetworkedComponent]
-[Access(typeof(SharedChargesSystem))]
-[AutoGenerateComponentState]
+/// <summary>
+/// Specifies the attached action has discrete charges, separate to a cooldown.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedChargesSystem))]
 public sealed partial class LimitedChargesComponent : Component
 {
+    [DataField, AutoNetworkedField]
+    public int LastCharges;
+
     /// <summary>
-    /// The maximum number of charges
+    ///     The max charges this action has.
     /// </summary>
-    [DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
-    [AutoNetworkedField]
-    public int MaxCharges = 3;
+    [DataField, AutoNetworkedField, Access(Other = AccessPermissions.Read)]
+    public int MaxCharges = 1;
 
     /// <summary>
-    /// The current number of charges
+    /// Last time charges was changed. Used to derive current charges.
     /// </summary>
-    [DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
-    [AutoNetworkedField]
-    public int Charges = 3;
+    [DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
+    public TimeSpan LastUpdate;
 }
index 7f95ef184e49a14a31e1493f94f8cd6ea0af2ab8..2eb05f8bfce809da3662996c29f6e0d96ebab665 100644 (file)
+using Content.Shared.Actions.Events;
 using Content.Shared.Charges.Components;
 using Content.Shared.Examine;
+using JetBrains.Annotations;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.Charges.Systems;
 
 public abstract class SharedChargesSystem : EntitySystem
 {
-    protected EntityQuery<LimitedChargesComponent> Query;
+    [Dependency] protected readonly IGameTiming _timing = default!;
+
+    /*
+     * Despite what a bunch of systems do you don't need to continuously tick linear number updates and can just derive it easily.
+     */
 
     public override void Initialize()
     {
         base.Initialize();
 
-        Query = GetEntityQuery<LimitedChargesComponent>();
-
         SubscribeLocalEvent<LimitedChargesComponent, ExaminedEvent>(OnExamine);
+
+        SubscribeLocalEvent<LimitedChargesComponent, ActionAttemptEvent>(OnChargesAttempt);
+        SubscribeLocalEvent<LimitedChargesComponent, MapInitEvent>(OnChargesMapInit);
+        SubscribeLocalEvent<LimitedChargesComponent, ActionPerformedEvent>(OnChargesPerformed);
     }
 
-    protected virtual void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
+    private void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
     {
         if (!args.IsInDetailsRange)
             return;
 
-        using (args.PushGroup(nameof(LimitedChargesComponent)))
+        var rechargeEnt = new Entity<LimitedChargesComponent?, AutoRechargeComponent?>(uid, comp, null);
+        var charges = GetCurrentCharges(rechargeEnt);
+        using var _ = args.PushGroup(nameof(LimitedChargesComponent));
+
+        args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", charges)));
+        if (charges == comp.MaxCharges)
         {
-            args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", comp.Charges)));
-            if (comp.Charges == comp.MaxCharges)
-            {
-                args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
-            }
+            args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
         }
+
+        // only show the recharging info if it's not full
+        if (charges == comp.MaxCharges || !TryComp<AutoRechargeComponent>(uid, out var recharge))
+            return;
+
+        rechargeEnt.Comp2 = recharge;
+        var timeRemaining = GetNextRechargeTime(rechargeEnt);
+        args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining.TotalSeconds.ToString("F1"))));
     }
 
-    /// <summary>
-    /// Tries to add a number of charges. If it over or underflows it will be clamped, wasting the extra charges.
-    /// </summary>
-    public virtual void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
+    private void OnChargesAttempt(Entity<LimitedChargesComponent> ent, ref ActionAttemptEvent args)
     {
-        if (!Query.Resolve(uid, ref comp, false))
+        if (args.Cancelled)
             return;
 
-        var old = comp.Charges;
-        comp.Charges = Math.Clamp(comp.Charges + change, 0, comp.MaxCharges);
-        if (comp.Charges != old)
-            Dirty(uid, comp);
+        var charges = GetCurrentCharges((ent.Owner, ent.Comp, null));
+
+        if (charges <= 0)
+        {
+            args.Cancelled = true;
+        }
+    }
+
+    private void OnChargesPerformed(Entity<LimitedChargesComponent> ent, ref ActionPerformedEvent args)
+    {
+        AddCharges((ent.Owner, ent.Comp), -1);
+    }
+
+    private void OnChargesMapInit(Entity<LimitedChargesComponent> ent, ref MapInitEvent args)
+    {
+        // If nothing specified use max.
+        if (ent.Comp.LastCharges == 0)
+        {
+            ent.Comp.LastCharges = ent.Comp.MaxCharges;
+        }
+        // If -1 used then we don't want any.
+        else if (ent.Comp.LastCharges < 0)
+        {
+            ent.Comp.LastCharges = 0;
+        }
+
+        ent.Comp.LastUpdate = _timing.CurTime;
+        Dirty(ent);
+    }
+
+    [Pure]
+    public bool HasCharges(Entity<LimitedChargesComponent?> action, int charges)
+    {
+        var current = GetCurrentCharges(action);
+
+        return current >= charges;
     }
 
     /// <summary>
-    /// Gets the limited charges component and returns true if there are no charges. Will return false if there is no limited charges component.
+    /// Adds the specified charges. Does not reset the accumulator.
     /// </summary>
-    public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null)
+    public void AddCharges(Entity<LimitedChargesComponent?> action, int addCharges)
     {
-        // can't be empty if there are no limited charges
-        if (!Query.Resolve(uid, ref comp, false))
+        if (addCharges == 0)
+            return;
+
+        action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
+
+        // 1. If we're going FROM max then set lastupdate to now (so it doesn't instantly recharge).
+        // 2. If we're going TO max then also set lastupdate to now.
+        // 3. Otherwise don't modify it.
+        // No idea if we go to 0 but future problem.
+
+        var lastCharges = GetCurrentCharges(action);
+        var charges = lastCharges + addCharges;
+
+        if (lastCharges == charges)
+            return;
+
+        if (charges == action.Comp.MaxCharges || lastCharges == action.Comp.MaxCharges)
+        {
+            action.Comp.LastUpdate = _timing.CurTime;
+        }
+
+        action.Comp.LastCharges = Math.Clamp(action.Comp.LastCharges + addCharges, 0, action.Comp.MaxCharges);
+        Dirty(action);
+    }
+
+    public bool TryUseCharge(Entity<LimitedChargesComponent?> entity)
+    {
+        return TryUseCharges(entity, 1);
+    }
+
+    public bool TryUseCharges(Entity<LimitedChargesComponent?> entity, int amount)
+    {
+        var current = GetCurrentCharges(entity);
+
+        if (current < amount)
+        {
             return false;
+        }
 
-        return comp.Charges <= 0;
+        AddCharges(entity, -amount);
+        return true;
     }
 
-    /// <summary>
-    /// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge.
-    /// </summary>
-    public void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
+    [Pure]
+    public bool IsEmpty(Entity<LimitedChargesComponent?> entity)
     {
-        AddCharges(uid, -1, comp);
+        return GetCurrentCharges(entity) == 0;
     }
 
     /// <summary>
-    /// Checks IsEmpty and uses a charge if it isn't empty.
+    /// Resets action charges to MaxCharges.
     /// </summary>
-    public bool TryUseCharge(Entity<LimitedChargesComponent?> ent)
+    public void ResetCharges(Entity<LimitedChargesComponent?> action)
     {
-        if (!Query.Resolve(ent, ref ent.Comp, false))
-            return true;
+        if (!Resolve(action.Owner, ref action.Comp, false))
+            return;
 
-        if (IsEmpty(ent, ent.Comp))
-            return false;
+        var charges = GetCurrentCharges((action.Owner, action.Comp, null));
 
-        UseCharge(ent, ent.Comp);
-        return true;
+        if (charges == action.Comp.MaxCharges)
+            return;
+
+        action.Comp.LastCharges = action.Comp.MaxCharges;
+        action.Comp.LastUpdate = _timing.CurTime;
+        Dirty(action);
+    }
+
+    public void SetCharges(Entity<LimitedChargesComponent?> action, int value)
+    {
+        action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
+
+        var adjusted = Math.Clamp(value, 0, action.Comp.MaxCharges);
+
+        if (action.Comp.LastCharges == adjusted)
+        {
+            return;
+        }
+
+        action.Comp.LastCharges = adjusted;
+        action.Comp.LastUpdate = _timing.CurTime;
+        Dirty(action);
     }
 
     /// <summary>
-    /// Gets the limited charges component and returns true if the number of charges remaining is less than the specified value.
-    /// Will return false if there is no limited charges component.
+    /// The next time a charge will be considered to be filled.
     /// </summary>
-    public bool HasInsufficientCharges(EntityUid uid, int requiredCharges, LimitedChargesComponent? comp = null)
+    /// <returns>0 timespan if invalid or no charges to generate.</returns>
+    [Pure]
+    public TimeSpan GetNextRechargeTime(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
     {
-        // can't be empty if there are no limited charges
-        if (!Resolve(uid, ref comp, false))
-            return false;
+        if (!Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, false))
+        {
+            return TimeSpan.Zero;
+        }
 
-        return comp.Charges < requiredCharges;
+        // Okay so essentially we need to get recharge time to full, then modulus that by the recharge timer which should be the next tick.
+        var fullTime = ((entity.Comp1.MaxCharges - entity.Comp1.LastCharges) * entity.Comp2.RechargeDuration) + entity.Comp1.LastUpdate;
+        var timeRemaining = fullTime - _timing.CurTime;
+
+        if (timeRemaining < TimeSpan.Zero)
+        {
+            return TimeSpan.Zero;
+        }
+
+        var nextChargeTime = timeRemaining.TotalSeconds % entity.Comp2.RechargeDuration.TotalSeconds;
+        return TimeSpan.FromSeconds(nextChargeTime);
     }
 
     /// <summary>
-    /// Uses up a specified number of charges. Must check HasInsufficentCharges beforehand to prevent using with insufficient remaining charges.
+    /// Derives the current charges of an entity.
     /// </summary>
-    public virtual void UseCharges(EntityUid uid, int chargesUsed, LimitedChargesComponent? comp = null)
+    [Pure]
+    public int GetCurrentCharges(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
     {
-        AddCharges(uid, -chargesUsed, comp);
+        if (!Resolve(entity.Owner, ref entity.Comp1, false))
+        {
+            // I'm all in favor of nullable ints however null-checking return args against comp nullability is dodgy
+            // so we get this.
+            return -1;
+        }
+
+        var calculated = 0;
+
+        if (Resolve(entity.Owner, ref entity.Comp2, false) && entity.Comp2.RechargeDuration.TotalSeconds != 0.0)
+        {
+            calculated = (int)((_timing.CurTime - entity.Comp1.LastUpdate).TotalSeconds / entity.Comp2.RechargeDuration.TotalSeconds);
+        }
+
+        return Math.Clamp(entity.Comp1.LastCharges + calculated,
+            0,
+            entity.Comp1.MaxCharges);
     }
 }
index 9626f177192ff171fdf6ee079b93348a00ce64f1..7aa4303471df41d60ae614aa275fb292eb4b15da 100644 (file)
@@ -21,7 +21,7 @@ namespace Content.Shared.Emag.Systems;
 public sealed class EmagSystem : EntitySystem
 {
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly SharedChargesSystem _charges = default!;
+    [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly TagSystem _tag = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -61,8 +61,8 @@ public sealed class EmagSystem : EntitySystem
         if (_tag.HasTag(target, ent.Comp.EmagImmuneTag))
             return false;
 
-        TryComp<LimitedChargesComponent>(ent, out var charges);
-        if (_charges.IsEmpty(ent, charges))
+        Entity<LimitedChargesComponent?> chargesEnt = ent.Owner;
+        if (_sharedCharges.IsEmpty(chargesEnt))
         {
             _popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
             return false;
@@ -80,8 +80,8 @@ public sealed class EmagSystem : EntitySystem
 
         _adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
 
-        if (charges != null  && emaggedEvent.Handled)
-            _charges.UseCharge(ent, charges);
+        if (emaggedEvent.Handled)
+            _sharedCharges.TryUseCharge(chargesEnt);
 
         if (!emaggedEvent.Repeatable)
         {
index ce1628bacbf4ad702f7ea967a16521cf0fdaed25..39fa16f62233031b4f8fbd1b1bc8051e24a6f065 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Charges.Systems;
 using Content.Shared.DoAfter;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Magic.Components;
@@ -9,6 +10,7 @@ namespace Content.Shared.Magic;
 
 public sealed class SpellbookSystem : EntitySystem
 {
+    [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
     [Dependency] private readonly SharedMindSystem _mind = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly SharedActionsSystem _actions = default!;
@@ -30,11 +32,7 @@ public sealed class SpellbookSystem : EntitySystem
             if (spell == null)
                 continue;
 
-            int? charge = charges;
-            if (_actions.GetCharges(spell) != null)
-                charge = _actions.GetCharges(spell);
-
-            _actions.SetCharges(spell, charge < 0 ? null : charge);
+            _sharedCharges.SetCharges(spell.Value, charges);
             ent.Comp.Spells.Add(spell.Value);
         }
     }
@@ -75,7 +73,7 @@ public sealed class SpellbookSystem : EntitySystem
             {
                 EntityUid? actionId = null;
                 if (_actions.AddAction(args.Args.User, ref actionId, id))
-                    _actions.SetCharges(actionId, charges < 0 ? null : charges);
+                    _sharedCharges.SetCharges(actionId.Value, charges);
             }
         }
 
index cd8f7da76860e7cc236898d869b37625fe70435d..c02d6cfd9ac38e0ec57a9150a822b333dbf0374c 100644 (file)
@@ -22,7 +22,7 @@ public sealed class DashAbilitySystem : EntitySystem
 {
     [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
-    [Dependency] private readonly SharedChargesSystem _charges = default!;
+    [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
     [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly ExamineSystemShared _examine = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -79,7 +79,7 @@ public sealed class DashAbilitySystem : EntitySystem
             return;
         }
 
-        if (!_charges.TryUseCharge(uid))
+        if (!_sharedCharges.TryUseCharge(uid))
         {
             _popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
             return;
index 281b97a648acb8576e2d9a4efdb0cef65c3e9ad7..c2425b92648e18d0e1fbfc3510bc42aabdf038ca 100644 (file)
@@ -28,6 +28,7 @@ public sealed class EnergyKatanaSystem : EntitySystem
 
     private void OnCheckDash(Entity<EnergyKatanaComponent> ent, ref CheckDashEvent args)
     {
+        // Just use a whitelist fam
         if (!_ninja.IsNinja(args.User))
             args.Cancelled = true;
     }
index 4135b606e22aeac978a94dbbff8831d17ec135ea..2c0c6bcabcde5a93522e3c9e445cbcc22094861a 100644 (file)
@@ -11,6 +11,6 @@ public sealed partial class RCDAmmoComponent : Component
     /// How many charges are contained in this ammo cartridge.
     /// Can be partially transferred into an RCD, until it is empty then it gets deleted.
     /// </summary>
-    [DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+    [DataField, AutoNetworkedField]
     public int Charges = 30;
 }
index 9cb3c26485114e8d2441e90af31d1af9a517d508..eb770f28985d90f7ebc2b85ef4edce3383f6faef 100644 (file)
@@ -10,7 +10,7 @@ namespace Content.Shared.RCD.Systems;
 
 public sealed class RCDAmmoSystem : EntitySystem
 {
-    [Dependency] private readonly SharedChargesSystem _charges = default!;
+    [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
 
@@ -41,9 +41,10 @@ public sealed class RCDAmmoSystem : EntitySystem
             !TryComp<LimitedChargesComponent>(target, out var charges))
             return;
 
+        var current = _sharedCharges.GetCurrentCharges((target, charges));
         var user = args.User;
         args.Handled = true;
-        var count = Math.Min(charges.MaxCharges - charges.Charges, comp.Charges);
+        var count = Math.Min(charges.MaxCharges - current, comp.Charges);
         if (count <= 0)
         {
             _popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-full"), target, user);
@@ -51,7 +52,7 @@ public sealed class RCDAmmoSystem : EntitySystem
         }
 
         _popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-refilled"), target, user);
-        _charges.AddCharges(target, count, charges);
+        _sharedCharges.AddCharges(target, count);
         comp.Charges -= count;
         Dirty(uid, comp);
 
index 83d6660e8e4702da9ee0a42e8e2d977618c10d58..2025f2f3cca25eb0c629f1b9520d937d0f1a4e85 100644 (file)
@@ -33,7 +33,7 @@ public sealed class RCDSystem : EntitySystem
     [Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
     [Dependency] private readonly FloorTileSystem _floors = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedChargesSystem _charges = default!;
+    [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -43,7 +43,6 @@ public sealed class RCDSystem : EntitySystem
     [Dependency] private readonly SharedMapSystem _mapSystem = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly TagSystem _tags = default!;
-    [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
 
     private readonly int _instantConstructionDelay = 0;
     private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0";
@@ -133,7 +132,7 @@ public sealed class RCDSystem : EntitySystem
         if (!location.IsValid(EntityManager))
             return;
 
-        var gridUid = _transformSystem.GetGrid(location);
+        var gridUid = _transform.GetGrid(location);
 
         if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
         {
@@ -239,7 +238,7 @@ public sealed class RCDSystem : EntitySystem
         // Ensure the RCD operation is still valid
         var location = GetCoordinates(args.Event.Location);
 
-        var gridUid = _transformSystem.GetGrid(location);
+        var gridUid = _transform.GetGrid(location);
 
         if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
         {
@@ -272,7 +271,7 @@ public sealed class RCDSystem : EntitySystem
 
         var location = GetCoordinates(args.Location);
 
-        var gridUid = _transformSystem.GetGrid(location);
+        var gridUid = _transform.GetGrid(location);
 
         if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
             return;
@@ -289,7 +288,7 @@ public sealed class RCDSystem : EntitySystem
 
         // Play audio and consume charges
         _audio.PlayPredicted(component.SuccessSound, uid, args.User);
-        _charges.UseCharges(uid, args.Cost);
+        _sharedCharges.AddCharges(uid, -args.Cost);
     }
 
     private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
@@ -321,10 +320,10 @@ public sealed class RCDSystem : EntitySystem
         var prototype = _protoManager.Index(component.ProtoId);
 
         // Check that the RCD has enough ammo to get the job done
-        TryComp<LimitedChargesComponent>(uid, out var charges);
+        var charges = _sharedCharges.GetCurrentCharges(uid);
 
         // Both of these were messages were suppose to be predicted, but HasInsufficientCharges wasn't being checked on the client for some reason?
-        if (_charges.IsEmpty(uid, charges))
+        if (charges == 0)
         {
             if (popMsgs)
                 _popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
@@ -332,7 +331,7 @@ public sealed class RCDSystem : EntitySystem
             return false;
         }
 
-        if (_charges.HasInsufficientCharges(uid, prototype.Cost, charges))
+        if (prototype.Cost > charges)
         {
             if (popMsgs)
                 _popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);
index b1c68f9d0eeba3d3f8743a180ba0acfd2c5753bd..e1da9bcf1ff42b1499b7da7e2a0b8185e973b0a9 100644 (file)
@@ -93,8 +93,9 @@
   name: Break Free
   description: Activating your freedom implant will free you from any hand restraints
   components:
+  - type: LimitedCharges
+    maxCharges: 3
   - type: InstantAction
-    charges: 3
     checkCanInteract: false
     itemIconStyle: BigAction
     priority: -20
   name: Activate EMP
   description: Triggers a small EMP pulse around you
   components:
+  - type: LimitedCharges
+    maxCharges: 3
   - type: InstantAction
     checkCanInteract: false
-    charges: 3
     useDelay: 5
     itemIconStyle: BigAction
     priority: -20
   name: SCRAM!
   description: Randomly teleports you within a large distance.
   components:
+  - type: LimitedCharges
+    maxCharges: 2
   - type: InstantAction
     checkCanInteract: false
-    charges: 2
     useDelay: 5
     itemIconStyle: BigAction
     priority: -20
   components:
   - type: ConfirmableAction
     popup: dna-scrambler-action-popup
+  - type: LimitedCharges
+    maxCharges: 1
   - type: InstantAction
-    charges: 1
     itemIconStyle: BigAction
     priority: -20
     icon:
index 9b27ad0bc6596974ebcc02222bbfee52860b07ec..a6ab09699fb03db8d1742adeb9c78ebcc83cd65c 100644 (file)
   - type: Flash
   - type: LimitedCharges
     maxCharges: 3
-    charges: 3
   - type: AutoRecharge
     rechargeDuration: 30
   - type: MeleeWeapon
index 89101e34ff466884d1aa2b0a54b64dc01069bcfb..f5ea2e6f271f90b0ee40ec8b78101b47e33f49c0 100644 (file)
@@ -88,7 +88,6 @@
     - type: Flash
     - type: LimitedCharges
       maxCharges: 15
-      charges: 15
     - type: MeleeWeapon
       damage:
         types:
index 11f40764b7d0618b93b28fb585af047384399d9f..d84dec120addf7c48772e445b08ff2e562dfa233 100644 (file)
     - Deconstruct
   - type: LimitedCharges
     maxCharges: 30
-    charges: 30
   - type: Sprite
     sprite: Objects/Tools/rcd.rsi
     state: icon
   suffix: Empty
   components:
   - type: LimitedCharges
-    charges: 0
+    lastCharges: -1
 
 - type: entity
   id: RCDRecharging
   components:
   - type: LimitedCharges
     maxCharges: 20
-    charges: 20
   - type: AutoRecharge
     rechargeDuration: 10
 
index 44e98538adc53bf6e031d4bc653aba6c4714163f..a87f380646684910eda63fb976ad00611b7bfda9 100644 (file)
@@ -89,7 +89,6 @@
   - type: DashAbility
   - type: LimitedCharges
     maxCharges: 3
-    charges: 3
   - type: AutoRecharge
     rechargeDuration: 20
   - type: Clothing
index 59889ce6492153ad46a2674fd5ae640e5af74710..ff54af9518471774ba61dfc4d73e8ca5ff2afbb5 100644 (file)
     - type: Flash
     - type: LimitedCharges
       maxCharges: 5
-      charges: 5
     - type: MeleeWeapon
       wideAnimationRotation: 180
       damage:
   components:
     - type: LimitedCharges
       maxCharges: 2
-      charges: 2
 
 - type: entity
   name: portable flasher
index a9609c4f8af4e0ebdb5db2b3b1fab9db03b0177f..d36afb49d911d64639add850cc07977114ca8714 100644 (file)
@@ -3,9 +3,10 @@
   name: Animate
   description: Bring an inanimate object to life!
   components:
+  - type: LimitedCharges
+    maxCharges: 5
   - type: EntityTargetAction
     useDelay: 0
-    charges: 5
     itemIconStyle: BigAction
     whitelist:
       components:
index 71bbc096c5550a48e1852cc6ee8a8014867e454d..eee8b1fc8a449099613d12638e5ab06b83ee53af 100644 (file)
@@ -30,8 +30,6 @@
   description: Fires a fireball, but faster!
   components:
   - type: WorldTargetAction
-    useDelay: 10
-    renewCharges: true
     itemIconStyle: BigAction
     checkCanAccess: false
     raiseOnUser: true
@@ -52,8 +50,6 @@
   description: The fastest fireball in the west!
   components:
     - type: WorldTargetAction
-      useDelay: 8
-      renewCharges: true
       itemIconStyle: BigAction
       checkCanAccess: false
       raiseOnUser: true
index 0582899495f16361ad74c874f91a6f7aae0714c8..8bfb30b88708ea5e4ca9cfd7fc9a7c474936761b 100644 (file)
 - type: entity
   id: ActionRgbLight
   components:
+  - type: LimitedCharges
+    maxCharges: 25
   - type: EntityTargetAction
     whitelist: { components: [ PointLight ] }
-    charges: 25
     sound: /Audio/Magic/blink.ogg
     event: !type:ChangeComponentsSpellEvent
       toAdd: