From: Centronias Date: Wed, 17 Dec 2025 22:52:32 +0000 (-0800) Subject: Adds BallisticAmmoSelfRefillerComponent (#38537) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=3c15d9f312db4543ff6b10ff502ae392bf36eafd;p=space-station-14.git Adds BallisticAmmoSelfRefillerComponent (#38537) * 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 --- 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 c2dfa23fcb..c501c0aa79 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 2061392200..6a61191bfe 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -658,6 +658,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 467ac0c848..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: 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml index d6c55c822c..74010b3f42 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml @@ -126,14 +126,16 @@ - 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml index 71a6714315..21dc8b7f31 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: 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