diff --git a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoSelfRefillerComponent.cs b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoSelfRefillerComponent.cs
new file mode 100644
index 0000000000..acaeca89dc
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoSelfRefillerComponent.cs
@@ -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;
+
+///
+/// This component, analogous to , will attempt insert ballistic ammunition
+/// into its owner's .
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause,
+ Access(typeof(SharedGunSystem))]
+public sealed partial class BallisticAmmoSelfRefillerComponent : Component
+{
+ ///
+ /// True if the refilling behavior is active, false otherwise.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool AutoRefill = true;
+
+ ///
+ /// How often a new piece of ammunition is inserted into the owner's .
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan AutoRefillRate = TimeSpan.FromSeconds(1);
+
+ ///
+ /// If true, causes the refilling behavior to be delayed by at least after
+ /// the owner is fired.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool FiringPausesAutoRefill = false;
+
+ ///
+ /// How long to pause for if is true.
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan AutoRefillPauseDuration = TimeSpan.Zero;
+
+ ///
+ /// What entity to spawn and attempt to insert into the owner. If null, uses
+ /// . If that's also null, this component does nothing but log
+ /// errors.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntProtoId? AmmoProto;
+
+ ///
+ /// If true, EMPs will pause this component's behavior.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool AffectedByEmp = false;
+
+ ///
+ /// When the next auto refill should occur. This is just implementation state.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+ public TimeSpan NextAutoRefill = TimeSpan.Zero;
+}
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
index 3addb3b783..54b07debf9 100644
--- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
@@ -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(OnBallisticInit);
@@ -30,6 +31,14 @@ public abstract partial class SharedGunSystem
SubscribeLocalEvent(OnBallisticAfterInteract);
SubscribeLocalEvent(OnBallisticAmmoFillDoAfter);
SubscribeLocalEvent(OnBallisticUse);
+
+ SubscribeLocalEvent(OnBallisticRefillerMapInit);
+ SubscribeLocalEvent(OnRefillerEmpPulsed);
+ }
+
+ private void OnBallisticRefillerMapInit(Entity 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(uid, out var refiller))
+ {
+ PauseSelfRefill((uid, refiller));
+ }
}
}
@@ -271,6 +274,73 @@ public abstract partial class SharedGunSystem
args.Capacity = component.Capacity;
}
+ ///
+ /// Causes to pause its refilling for either at least
+ /// (if not null) or the entity's . If the
+ /// entity's next refill would occur after the pause duration, this function has no effect.
+ ///
+ public void PauseSelfRefill(
+ Entity 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));
+ }
+ }
+
+ ///
+ /// Returns true if the given 's ballistic ammunition is full, false otherwise.
+ ///
+ public bool IsFull(Entity entity)
+ {
+ return GetBallisticShots(entity.Comp) >= entity.Comp.Capacity;
+ }
+
+ ///
+ /// Returns whether or not can be inserted into , based on
+ /// available space and whitelists.
+ ///
+ public bool CanInsertBallistic(Entity entity, EntityUid inserted)
+ {
+ return !_whitelistSystem.IsWhitelistFailOrNull(entity.Comp.Whitelist, inserted) &&
+ !IsFull(entity);
+ }
+
+ ///
+ /// Attempts to insert into as ammunition. Returns true on
+ /// success, false otherwise.
+ ///
+ public bool TryBallisticInsert(
+ Entity 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(uid, out var appearance))
@@ -290,6 +360,70 @@ public abstract partial class SharedGunSystem
UpdateAmmoCount(entity.Owner);
Dirty(entity);
}
+
+ private void OnRefillerEmpPulsed(Entity entity, ref EmpPulseEvent args)
+ {
+ if (!entity.Comp.AffectedByEmp)
+ return;
+
+ PauseSelfRefill(entity, args.Duration);
+ }
+
+ private void UpdateBallistic(float frameTime)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var refiller, out var ammo))
+ {
+ BallisticSelfRefillerUpdate((uid, ammo, refiller));
+ }
+ }
+
+ private void BallisticSelfRefillerUpdate(
+ Entity 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)}?");
+ }
+ }
+ }
}
///
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
index cd2a3edf11..1fd2bc2478 100644
--- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
@@ -682,6 +682,7 @@ public abstract partial class SharedGunSystem : EntitySystem
public override void Update(float frameTime)
{
UpdateBattery(frameTime);
+ UpdateBallistic(frameTime);
}
}
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml
index 36a4ee0f49..b499ea46b3 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml
@@ -116,12 +116,14 @@
- type: ContainerContainer
containers:
ballistic-ammo: !type:Container
- - type: BatteryAmmoProvider
+ - type: BallisticAmmoProvider
+ whitelist:
+ tags:
+ - CartridgeLightRifle
+ capacity: 100
proto: CartridgeLightRifle
- fireCost: 100
- - type: Battery
- maxCharge: 10000
- startingCharge: 10000
- - type: BatterySelfRecharger
- autoRechargeRate: 25
+ cycleable: false # No synthesizing ammo for your syndicate masters.
+ - type: BallisticAmmoSelfRefiller
+ autoRefillRate: 4s
+ affectedByEmp: true
- type: AmmoCounter
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml
index 0a2a4d544d..6dd8977341 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml
@@ -130,14 +130,16 @@
- type: ContainerContainer
containers:
ballistic-ammo: !type:Container
- - type: BatteryAmmoProvider
- proto: BulletPistol
- fireCost: 100
- - type: Battery
- maxCharge: 1000
- startingCharge: 1000
- - type: BatterySelfRecharger
- 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
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml
index 4b6bfe3cf9..d5a094aea6 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml
@@ -152,14 +152,16 @@
- type: ContainerContainer
containers:
ballistic-ammo: !type:Container
- - type: BatteryAmmoProvider
+ - type: BallisticAmmoProvider
+ whitelist:
+ tags:
+ - CartridgePistol
+ capacity: 30
proto: CartridgePistol
- fireCost: 100
- - type: Battery
- maxCharge: 3000
- startingCharge: 3000
- - type: BatterySelfRecharger
- autoRechargeRate: 25
+ cycleable: false # No synthesizing ammo for your syndicate masters.
+ - type: BallisticAmmoSelfRefiller
+ autoRefillRate: 4s
+ affectedByEmp: true
- type: AmmoCounter
- type: entity