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(OnBallisticInit); SubscribeLocalEvent(OnBallisticMapInit); SubscribeLocalEvent(OnBallisticTakeAmmo); SubscribeLocalEvent(OnBallisticAmmoCount); SubscribeLocalEvent(OnBallisticExamine); SubscribeLocalEvent>(OnBallisticVerb); SubscribeLocalEvent(OnBallisticInteractUsing); SubscribeLocalEvent(OnBallisticAfterInteract); SubscribeLocalEvent(OnBallisticAmmoFillDoAfter); SubscribeLocalEvent(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(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(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 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(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(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 entity, int count) { if (entity.Comp.UnspawnedCount == count) return; entity.Comp.UnspawnedCount = count; UpdateBallisticAppearance(entity.Owner, entity.Comp); UpdateAmmoCount(entity.Owner); Dirty(entity); } } /// /// DoAfter event for filling one ballistic ammo provider from another. /// [Serializable, NetSerializable] public sealed partial class AmmoFillDoAfterEvent : SimpleDoAfterEvent { }