]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Adds BallisticAmmoSelfRefillerComponent (#38537)
authorCentronias <me@centronias.com>
Wed, 17 Dec 2025 22:52:32 +0000 (14:52 -0800)
committerGitHub <noreply@github.com>
Wed, 17 Dec 2025 22:52:32 +0000 (22:52 +0000)
* Adds BallisticAmmoSelfRefillerComponent

And uses it to replace battery-based refilling of the Syndicate L6 and Viper modules.

# Automagic Ballistic Ammo Refilling
- Add `BallisticAmmoSelfRefillerComponent`
- Handle `EmpPulseEvent` to pause refilling behavior for EMP's duration

# Supporting Changes
- Change `Content.Server.Weapons.Ranged.Systems.Update` override in `GunSystem.AutoFire.cs` to `UpdateAutoFire`
- Add `Content.Server.Weapons.Ranged.Systems.Update` to `GunSystem.cs` so that it can call `UpdateAutoFire` and `UpdateBallistic`
- Add public methods to GunSystem for use by refilling implementation
  - PauseSelfRefill
  - IsFullBallistic (same as #299)
  - CanInsertBallistic (same as #299)
  - TryBallisticInsert (same as #299)

* _timing -> Timing

* unspawned count stuff

* imagine building the code before pushing

* - apply to c20r ROW
- make predicted/shared

* revert server system import only changes

* oop

* o great and wise Slarti

* Scar comments

* field deltas + correct serializer

* review

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
Content.Shared/Weapons/Ranged/Components/BallisticAmmoSelfRefillerComponent.cs [new file with mode: 0644]
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml

diff --git a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoSelfRefillerComponent.cs b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoSelfRefillerComponent.cs
new file mode 100644 (file)
index 0000000..acaeca8
--- /dev/null
@@ -0,0 +1,61 @@
+using Content.Shared.Power.Components;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+/// <summary>
+/// This component, analogous to <see cref="BatterySelfRechargerComponent"/>, will attempt insert ballistic ammunition
+/// into its owner's <see cref="BallisticAmmoProviderComponent"/>.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause,
+ Access(typeof(SharedGunSystem))]
+public sealed partial class BallisticAmmoSelfRefillerComponent : Component
+{
+    /// <summary>
+    /// True if the refilling behavior is active, false otherwise.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool AutoRefill = true;
+
+    /// <summary>
+    /// How often a new piece of ammunition is inserted into the owner's <see cref="BallisticAmmoProviderComponent"/>.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan AutoRefillRate = TimeSpan.FromSeconds(1);
+
+    /// <summary>
+    /// If true, causes the refilling behavior to be delayed by at least <see cref="AutoRefillPauseDuration"/> after
+    /// the owner is fired.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool FiringPausesAutoRefill = false;
+
+    /// <summary>
+    /// How long to pause for if <see cref="FiringPausesAutoRefill"/> is true.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan AutoRefillPauseDuration = TimeSpan.Zero;
+
+    /// <summary>
+    /// What entity to spawn and attempt to insert into the owner. If null, uses
+    /// <see cref="BallisticAmmoProviderComponent.Proto"/>. If that's also null, this component does nothing but log
+    /// errors.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntProtoId? AmmoProto;
+
+    /// <summary>
+    /// If true, EMPs will pause this component's behavior.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool AffectedByEmp = false;
+
+    /// <summary>
+    /// When the next auto refill should occur. This is just implementation state.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+    public TimeSpan NextAutoRefill = TimeSpan.Zero;
+}
index c2dfa23fcb03a7d24d36be5ea78f287724a63687..c501c0aa792f4b2d833f4694cdc756a0b387d73b 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.DoAfter;
+using Content.Shared.Emp;
 using Content.Shared.Examine;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
@@ -16,7 +17,7 @@ public abstract partial class SharedGunSystem
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly SharedInteractionSystem _interaction = default!;
 
-
+    [MustCallBase]
     protected virtual void InitializeBallistic()
     {
         SubscribeLocalEvent<BallisticAmmoProviderComponent, ComponentInit>(OnBallisticInit);
@@ -30,6 +31,14 @@ public abstract partial class SharedGunSystem
         SubscribeLocalEvent<BallisticAmmoProviderComponent, AfterInteractEvent>(OnBallisticAfterInteract);
         SubscribeLocalEvent<BallisticAmmoProviderComponent, AmmoFillDoAfterEvent>(OnBallisticAmmoFillDoAfter);
         SubscribeLocalEvent<BallisticAmmoProviderComponent, UseInHandEvent>(OnBallisticUse);
+
+        SubscribeLocalEvent<BallisticAmmoSelfRefillerComponent, MapInitEvent>(OnBallisticRefillerMapInit);
+        SubscribeLocalEvent<BallisticAmmoSelfRefillerComponent, EmpPulseEvent>(OnRefillerEmpPulsed);
+    }
+
+    private void OnBallisticRefillerMapInit(Entity<BallisticAmmoSelfRefillerComponent> entity, ref MapInitEvent args)
+    {
+        entity.Comp.NextAutoRefill = Timing.CurTime + entity.Comp.AutoRefillRate;
     }
 
     private void OnBallisticUse(EntityUid uid, BallisticAmmoProviderComponent component, UseInHandEvent args)
@@ -46,20 +55,8 @@ public abstract partial class SharedGunSystem
         if (args.Handled)
             return;
 
-        if (_whitelistSystem.IsWhitelistFailOrNull(component.Whitelist, args.Used))
-            return;
-
-        if (GetBallisticShots(component) >= component.Capacity)
-            return;
-
-        component.Entities.Add(args.Used);
-        Containers.Insert(args.Used, component.Container);
-        // Not predicted so
-        Audio.PlayPredicted(component.SoundInsert, uid, args.User);
-        args.Handled = true;
-        UpdateBallisticAppearance(uid, component);
-        UpdateAmmoCount(args.Target);
-        DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities));
+        if (TryBallisticInsert((uid, component), args.Used, args.User))
+            args.Handled = true;
     }
 
     private void OnBallisticAfterInteract(EntityUid uid, BallisticAmmoProviderComponent component, AfterInteractEvent args)
@@ -242,23 +239,29 @@ public abstract partial class SharedGunSystem
     {
         for (var i = 0; i < args.Shots; i++)
         {
-            EntityUid entity;
-
+            EntityUid? ammoEntity = null;
             if (component.Entities.Count > 0)
             {
-                entity = component.Entities[^1];
-
-                args.Ammo.Add((entity, EnsureShootable(entity)));
+                var existingEnt = component.Entities[^1];
                 component.Entities.RemoveAt(component.Entities.Count - 1);
                 DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities));
-                Containers.Remove(entity, component.Container);
+                Containers.Remove(existingEnt, component.Container);
+                ammoEntity = existingEnt;
             }
             else if (component.UnspawnedCount > 0)
             {
                 component.UnspawnedCount--;
                 DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount));
-                entity = Spawn(component.Proto, args.Coordinates);
-                args.Ammo.Add((entity, EnsureShootable(entity)));
+                ammoEntity = Spawn(component.Proto, args.Coordinates);
+            }
+
+            if (ammoEntity is { } ent)
+            {
+                args.Ammo.Add((ent, EnsureShootable(ent)));
+                if (TryComp<BallisticAmmoSelfRefillerComponent>(uid, out var refiller))
+                {
+                    PauseSelfRefill((uid, refiller));
+                }
             }
         }
 
@@ -271,6 +274,73 @@ public abstract partial class SharedGunSystem
         args.Capacity = component.Capacity;
     }
 
+    /// <summary>
+    /// Causes <paramref name="entity"/> to pause its refilling for either at least <paramref name="overridePauseDuration"/>
+    /// (if not null) or the entity's <see cref="BallisticAmmoSelfRefillerComponent.AutoRefillPauseDuration"/>. If the
+    /// entity's next refill would occur after the pause duration, this function has no effect.
+    /// </summary>
+    public void PauseSelfRefill(
+        Entity<BallisticAmmoSelfRefillerComponent> entity,
+        TimeSpan? overridePauseDuration = null
+    )
+    {
+        if (overridePauseDuration == null && !entity.Comp.FiringPausesAutoRefill)
+            return;
+
+        var nextRefillByPause = Timing.CurTime + (overridePauseDuration ?? entity.Comp.AutoRefillPauseDuration);
+        if (nextRefillByPause > entity.Comp.NextAutoRefill)
+        {
+            entity.Comp.NextAutoRefill = nextRefillByPause;
+            DirtyField(entity.AsNullable(), nameof(BallisticAmmoSelfRefillerComponent.NextAutoRefill));
+        }
+    }
+
+    /// <summary>
+    /// Returns true if the given <paramref name="entity"/>'s ballistic ammunition is full, false otherwise.
+    /// </summary>
+    public bool IsFull(Entity<BallisticAmmoProviderComponent> entity)
+    {
+        return GetBallisticShots(entity.Comp) >= entity.Comp.Capacity;
+    }
+
+    /// <summary>
+    /// Returns whether or not <paramref name="inserted"/> can be inserted into <paramref name="entity"/>, based on
+    /// available space and whitelists.
+    /// </summary>
+    public bool CanInsertBallistic(Entity<BallisticAmmoProviderComponent> entity, EntityUid inserted)
+    {
+        return !_whitelistSystem.IsWhitelistFailOrNull(entity.Comp.Whitelist, inserted) &&
+               !IsFull(entity);
+    }
+
+    /// <summary>
+    /// Attempts to insert <paramref name="inserted"/> into <paramref name="entity"/> as ammunition. Returns true on
+    /// success, false otherwise.
+    /// </summary>
+    public bool TryBallisticInsert(
+        Entity<BallisticAmmoProviderComponent> entity,
+        EntityUid inserted,
+        EntityUid? user,
+        bool suppressInsertionSound = false
+    )
+    {
+        if (!CanInsertBallistic(entity, inserted))
+            return false;
+
+        entity.Comp.Entities.Add(inserted);
+        Containers.Insert(inserted, entity.Comp.Container);
+        if (!suppressInsertionSound)
+        {
+            Audio.PlayPredicted(entity.Comp.SoundInsert, entity, user);
+        }
+
+        UpdateBallisticAppearance(entity, entity.Comp);
+        UpdateAmmoCount(entity);
+        DirtyField(entity.AsNullable(), nameof(BallisticAmmoProviderComponent.Entities));
+
+        return true;
+    }
+
     public void UpdateBallisticAppearance(EntityUid uid, BallisticAmmoProviderComponent component)
     {
         if (!Timing.IsFirstTimePredicted || !TryComp<AppearanceComponent>(uid, out var appearance))
@@ -290,6 +360,70 @@ public abstract partial class SharedGunSystem
         UpdateAmmoCount(entity.Owner);
         Dirty(entity);
     }
+
+    private void OnRefillerEmpPulsed(Entity<BallisticAmmoSelfRefillerComponent> entity, ref EmpPulseEvent args)
+    {
+        if (!entity.Comp.AffectedByEmp)
+            return;
+
+        PauseSelfRefill(entity, args.Duration);
+    }
+
+    private void UpdateBallistic(float frameTime)
+    {
+        var query = EntityQueryEnumerator<BallisticAmmoSelfRefillerComponent, BallisticAmmoProviderComponent>();
+        while (query.MoveNext(out var uid, out var refiller, out var ammo))
+        {
+            BallisticSelfRefillerUpdate((uid, ammo, refiller));
+        }
+    }
+
+    private void BallisticSelfRefillerUpdate(
+        Entity<BallisticAmmoProviderComponent, BallisticAmmoSelfRefillerComponent> entity
+    )
+    {
+        var ammo = entity.Comp1;
+        var refiller = entity.Comp2;
+        if (Timing.CurTime < refiller.NextAutoRefill)
+            return;
+
+        refiller.NextAutoRefill += refiller.AutoRefillRate;
+        DirtyField(entity, refiller, nameof(BallisticAmmoSelfRefillerComponent.NextAutoRefill));
+
+        if (!refiller.AutoRefill || IsFull(entity))
+            return;
+
+        if (refiller.AmmoProto is not { } refillerAmmoProto)
+        {
+            // No ammo proto on the refiller, so just increment the unspawned count on the provider
+            // if it has an ammo proto.
+            if (ammo.Proto is null)
+            {
+                Log.Error(
+                    $"Neither of {entity}'s {nameof(BallisticAmmoSelfRefillerComponent)}'s or {nameof(BallisticAmmoProviderComponent)}'s ammunition protos is specified. This is a configuration error as it means {nameof(BallisticAmmoSelfRefillerComponent)} cannot do anything.");
+                return;
+            }
+
+            SetBallisticUnspawned(entity, ammo.UnspawnedCount + 1);
+        }
+        else if (ammo.Proto == refillerAmmoProto)
+        {
+            // The ammo proto on the refiller and the provider match. Add an unspawned ammo.
+            SetBallisticUnspawned(entity, ammo.UnspawnedCount + 1);
+        }
+        else
+        {
+            // Can't use unspawned ammo, so spawn an entity and try to insert it.
+            var ammoEntity = PredictedSpawnAttachedTo(refiller.AmmoProto, Transform(entity).Coordinates);
+            var insertSucceeded = TryBallisticInsert(entity, ammoEntity, null, suppressInsertionSound: true);
+            if (!insertSucceeded)
+            {
+                PredictedQueueDel(ammoEntity);
+                Log.Error(
+                    $"Failed to insert ammo {ammoEntity} into non-full {entity}. This is a configuration error. Is the {nameof(BallisticAmmoSelfRefillerComponent)}'s {nameof(BallisticAmmoSelfRefillerComponent.AmmoProto)} incorrect for the {nameof(BallisticAmmoProviderComponent)}'s {nameof(BallisticAmmoProviderComponent.Whitelist)}?");
+            }
+        }
+    }
 }
 
 /// <summary>
index 2061392200c3a32474db5aef3485cae7518cf6af..6a61191bfedf42afc5a83372f25894b87845e56d 100644 (file)
@@ -658,6 +658,7 @@ public abstract partial class SharedGunSystem : EntitySystem
     public override void Update(float frameTime)
     {
         UpdateBattery(frameTime);
+        UpdateBallistic(frameTime);
     }
 }
 
index 467ac0c8484f4ce2e8281e3d1021a7b62ac4a5e1..b499ea46b37c537c6807ce5e26cd6e639ab7ae14 100644 (file)
     - type: ContainerContainer
       containers:
         ballistic-ammo: !type:Container
-    - type: BatteryAmmoProvider
+    - type: BallisticAmmoProvider
+      whitelist:
+        tags:
+        - CartridgeLightRifle
+      capacity: 100
       proto: CartridgeLightRifle
-      fireCost: 100
-    - type: PredictedBattery
-      maxCharge: 10000
-      startingCharge: 10000
-    - type: PredictedBatterySelfRecharger
-      autoRechargeRate: 25
+      cycleable: false # No synthesizing ammo for your syndicate masters.
+    - type: BallisticAmmoSelfRefiller
+      autoRefillRate: 4s
+      affectedByEmp: true
     - type: AmmoCounter
index d6c55c822c8417cb6a924175fd01641d40d45688..74010b3f42b3f2bbdf28f240f59a51e79161b273 100644 (file)
   - type: ContainerContainer
     containers:
       ballistic-ammo: !type:Container
-  - type: BatteryAmmoProvider
-    proto: BulletPistol
-    fireCost: 100
-  - type: PredictedBattery
-    maxCharge: 1000
-    startingCharge: 1000
-  - type: PredictedBatterySelfRecharger
-    autoRechargeRate: 25
+  - type: BallisticAmmoProvider
+    whitelist:
+      tags:
+      - CartridgePistol
+    capacity: 10
+    proto: CartridgePistol
+    cycleable: false # No synthesizing ammo for your syndicate masters.
+  - type: BallisticAmmoSelfRefiller
+    autoRefillRate: 4s
+    affectedByEmp: true
   - type: AmmoCounter
 
 - type: entity
index 71a67143150cd6b5d1ed356fdca988378313b2e0..21dc8b7f31b6805e3a3c178e29ecbce17cf3dfc8 100644 (file)
   - type: ContainerContainer
     containers:
       ballistic-ammo: !type:Container
-  - type: BatteryAmmoProvider
+  - type: BallisticAmmoProvider
+    whitelist:
+      tags:
+      - CartridgePistol
+    capacity: 30
     proto: CartridgePistol
-    fireCost: 100
-  - type: PredictedBattery
-    maxCharge: 3000
-    startingCharge: 3000
-  - type: PredictedBatterySelfRecharger
-    autoRechargeRate: 25
+    cycleable: false # No synthesizing ammo for your syndicate masters.
+  - type: BallisticAmmoSelfRefiller
+    autoRefillRate: 4s
+    affectedByEmp: true
   - type: AmmoCounter
 
 - type: entity