--- /dev/null
+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;
+}
using Content.Shared.DoAfter;
+using Content.Shared.Emp;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
-
+ [MustCallBase]
protected virtual void InitializeBallistic()
{
SubscribeLocalEvent<BallisticAmmoProviderComponent, ComponentInit>(OnBallisticInit);
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)
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)
{
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));
+ }
}
}
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))
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>