Files
tbd-station-14/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
Sir Warock 5ec141b955 Fix Ammo Visuals Bug (#41362)
Fix Ammo Count Updating
2025-11-09 07:42:55 +00:00

302 lines
12 KiB
C#

using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared.Weapons.Ranged.Systems;
public abstract partial class SharedGunSystem
{
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
protected virtual void InitializeBallistic()
{
SubscribeLocalEvent<BallisticAmmoProviderComponent, ComponentInit>(OnBallisticInit);
SubscribeLocalEvent<BallisticAmmoProviderComponent, MapInitEvent>(OnBallisticMapInit);
SubscribeLocalEvent<BallisticAmmoProviderComponent, TakeAmmoEvent>(OnBallisticTakeAmmo);
SubscribeLocalEvent<BallisticAmmoProviderComponent, GetAmmoCountEvent>(OnBallisticAmmoCount);
SubscribeLocalEvent<BallisticAmmoProviderComponent, ExaminedEvent>(OnBallisticExamine);
SubscribeLocalEvent<BallisticAmmoProviderComponent, GetVerbsEvent<Verb>>(OnBallisticVerb);
SubscribeLocalEvent<BallisticAmmoProviderComponent, InteractUsingEvent>(OnBallisticInteractUsing);
SubscribeLocalEvent<BallisticAmmoProviderComponent, AfterInteractEvent>(OnBallisticAfterInteract);
SubscribeLocalEvent<BallisticAmmoProviderComponent, AmmoFillDoAfterEvent>(OnBallisticAmmoFillDoAfter);
SubscribeLocalEvent<BallisticAmmoProviderComponent, UseInHandEvent>(OnBallisticUse);
}
private void OnBallisticUse(EntityUid uid, BallisticAmmoProviderComponent component, UseInHandEvent args)
{
if (args.Handled)
return;
ManualCycle(uid, component, TransformSystem.GetMapCoordinates(uid), args.User);
args.Handled = true;
}
private void OnBallisticInteractUsing(EntityUid uid, BallisticAmmoProviderComponent component, InteractUsingEvent 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));
}
private void OnBallisticAfterInteract(EntityUid uid, BallisticAmmoProviderComponent component, AfterInteractEvent args)
{
if (args.Handled ||
!component.MayTransfer ||
!Timing.IsFirstTimePredicted ||
args.Target == null ||
args.Used == args.Target ||
Deleted(args.Target) ||
!TryComp<BallisticAmmoProviderComponent>(args.Target, out var targetComponent) ||
targetComponent.Whitelist == null)
{
return;
}
args.Handled = true;
// Continuous loading
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.FillDelay, new AmmoFillDoAfterEvent(), used: uid, target: args.Target, eventTarget: uid)
{
BreakOnMove = true,
BreakOnDamage = false,
NeedHand = true,
});
}
private void OnBallisticAmmoFillDoAfter(EntityUid uid, BallisticAmmoProviderComponent component, AmmoFillDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
if (Deleted(args.Target) ||
!TryComp<BallisticAmmoProviderComponent>(args.Target, out var target) ||
target.Whitelist == null)
return;
if (target.Entities.Count + target.UnspawnedCount == target.Capacity)
{
Popup(
Loc.GetString("gun-ballistic-transfer-target-full",
("entity", args.Target)),
args.Target,
args.User);
return;
}
if (component.Entities.Count + component.UnspawnedCount == 0)
{
Popup(
Loc.GetString("gun-ballistic-transfer-empty",
("entity", uid)),
uid,
args.User);
return;
}
void SimulateInsertAmmo(EntityUid ammo, EntityUid ammoProvider, EntityCoordinates coordinates)
{
// We call SharedInteractionSystem to raise contact events. Checks are already done by this point.
_interaction.InteractUsing(args.User, ammo, ammoProvider, coordinates, checkCanInteract: false, checkCanUse: false);
}
List<(EntityUid? Entity, IShootable Shootable)> ammo = new();
var evTakeAmmo = new TakeAmmoEvent(1, ammo, Transform(uid).Coordinates, args.User);
RaiseLocalEvent(uid, evTakeAmmo);
foreach (var (ent, _) in ammo)
{
if (ent == null)
continue;
if (_whitelistSystem.IsWhitelistFail(target.Whitelist, ent.Value))
{
Popup(
Loc.GetString("gun-ballistic-transfer-invalid",
("ammoEntity", ent.Value),
("targetEntity", args.Target.Value)),
uid,
args.User);
SimulateInsertAmmo(ent.Value, uid, Transform(uid).Coordinates);
}
else
{
// play sound to be cool
Audio.PlayPredicted(component.SoundInsert, uid, args.User);
SimulateInsertAmmo(ent.Value, args.Target.Value, Transform(args.Target.Value).Coordinates);
}
if (IsClientSide(ent.Value))
Del(ent.Value);
}
// repeat if there is more space in the target and more ammo to fill
var moreSpace = target.Entities.Count + target.UnspawnedCount < target.Capacity;
var moreAmmo = component.Entities.Count + component.UnspawnedCount > 0;
args.Repeat = moreSpace && moreAmmo;
}
private void OnBallisticVerb(EntityUid uid, BallisticAmmoProviderComponent component, GetVerbsEvent<Verb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands == null || !component.Cycleable)
return;
if (component.Cycleable)
{
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("gun-ballistic-cycle"),
Disabled = GetBallisticShots(component) == 0,
Act = () => ManualCycle(uid, component, TransformSystem.GetMapCoordinates(uid), args.User),
});
}
}
private void OnBallisticExamine(EntityUid uid, BallisticAmmoProviderComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", GetBallisticShots(component))));
}
private void ManualCycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates, EntityUid? user = null, GunComponent? gunComp = null)
{
if (!component.Cycleable)
return;
// Reset shotting for cycling
if (Resolve(uid, ref gunComp, false) &&
gunComp is { FireRateModified: > 0f } &&
!Paused(uid))
{
gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRateModified);
DirtyField(uid, gunComp, nameof(GunComponent.NextFire));
}
Audio.PlayPredicted(component.SoundRack, uid, user);
var shots = GetBallisticShots(component);
Cycle(uid, component, coordinates);
var text = Loc.GetString(shots == 0 ? "gun-ballistic-cycled-empty" : "gun-ballistic-cycled");
Popup(text, uid, user);
UpdateBallisticAppearance(uid, component);
UpdateAmmoCount(uid);
}
protected abstract void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates);
private void OnBallisticInit(EntityUid uid, BallisticAmmoProviderComponent component, ComponentInit args)
{
component.Container = Containers.EnsureContainer<Container>(uid, "ballistic-ammo");
// TODO: This is called twice though we need to support loading appearance data (and we need to call it on MapInit
// to ensure it's correct).
UpdateBallisticAppearance(uid, component);
}
private void OnBallisticMapInit(EntityUid uid, BallisticAmmoProviderComponent component, MapInitEvent args)
{
// TODO this should be part of the prototype, not set on map init.
// Alternatively, just track spawned count, instead of unspawned count.
if (component.Proto != null)
{
component.UnspawnedCount = Math.Max(0, component.Capacity - component.Container.ContainedEntities.Count);
UpdateBallisticAppearance(uid, component);
DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount));
}
}
protected int GetBallisticShots(BallisticAmmoProviderComponent component)
{
return component.Entities.Count + component.UnspawnedCount;
}
private void OnBallisticTakeAmmo(EntityUid uid, BallisticAmmoProviderComponent component, TakeAmmoEvent args)
{
for (var i = 0; i < args.Shots; i++)
{
EntityUid entity;
if (component.Entities.Count > 0)
{
entity = component.Entities[^1];
args.Ammo.Add((entity, EnsureShootable(entity)));
component.Entities.RemoveAt(component.Entities.Count - 1);
DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities));
Containers.Remove(entity, component.Container);
}
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)));
}
}
UpdateBallisticAppearance(uid, component);
}
private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, ref GetAmmoCountEvent args)
{
args.Count = GetBallisticShots(component);
args.Capacity = component.Capacity;
}
public void UpdateBallisticAppearance(EntityUid uid, BallisticAmmoProviderComponent component)
{
if (!Timing.IsFirstTimePredicted || !TryComp<AppearanceComponent>(uid, out var appearance))
return;
Appearance.SetData(uid, AmmoVisuals.AmmoCount, GetBallisticShots(component), appearance);
Appearance.SetData(uid, AmmoVisuals.AmmoMax, component.Capacity, appearance);
}
public void SetBallisticUnspawned(Entity<BallisticAmmoProviderComponent> entity, int count)
{
if (entity.Comp.UnspawnedCount == count)
return;
entity.Comp.UnspawnedCount = count;
UpdateBallisticAppearance(entity.Owner, entity.Comp);
UpdateAmmoCount(entity.Owner);
Dirty(entity);
}
}
/// <summary>
/// DoAfter event for filling one ballistic ammo provider from another.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class AmmoFillDoAfterEvent : SimpleDoAfterEvent
{
}