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