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.GameStates; using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.Weapons.Ranged.Systems; public abstract partial class SharedGunSystem { protected virtual void InitializeBallistic() { SubscribeLocalEvent(OnBallisticInit); SubscribeLocalEvent(OnBallisticMapInit); SubscribeLocalEvent(OnBallisticTakeAmmo); SubscribeLocalEvent(OnBallisticAmmoCount); SubscribeLocalEvent(OnBallisticExamine); SubscribeLocalEvent>(OnBallisticVerb); SubscribeLocalEvent(OnBallisticInteractUsing); SubscribeLocalEvent(OnBallisticAfterInteract); SubscribeLocalEvent(OnBallisticUse); } private void OnBallisticUse(EntityUid uid, BallisticAmmoProviderComponent component, UseInHandEvent args) { if (args.Handled) return; ManualCycle(uid, component, Transform(uid).MapPosition, args.User); args.Handled = true; } private void OnBallisticInteractUsing(EntityUid uid, BallisticAmmoProviderComponent component, InteractUsingEvent args) { if (args.Handled || component.Whitelist?.IsValid(args.Used, EntityManager) != true) return; if (GetBallisticShots(component) >= component.Capacity) return; component.Entities.Add(args.Used); component.Container.Insert(args.Used); // Not predicted so Audio.PlayPredicted(component.SoundInsert, uid, args.User); args.Handled = true; UpdateBallisticAppearance(uid, component); Dirty(component); } 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 BallisticAmmoProviderComponent? targetComponent) || targetComponent.Whitelist == null) { return; } args.Handled = true; if (targetComponent.Entities.Count + targetComponent.UnspawnedCount == targetComponent.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", args.Used)), args.Used, args.User); return; } void SimulateInsertAmmo(EntityUid ammo, EntityUid ammoProvider, EntityCoordinates coordinates) { var evInsert = new InteractUsingEvent(args.User, ammo, ammoProvider, coordinates); RaiseLocalEvent(ammoProvider, evInsert); } List<(EntityUid? Entity, IShootable Shootable)> ammo = new(); var evTakeAmmo = new TakeAmmoEvent(1, ammo, Transform(args.Used).Coordinates, args.User); RaiseLocalEvent(args.Used, evTakeAmmo); foreach (var (ent, _) in ammo) { if (ent == null) continue; if (!targetComponent.Whitelist.IsValid(ent.Value)) { Popup( Loc.GetString("gun-ballistic-transfer-invalid", ("ammoEntity", ent.Value), ("targetEntity", args.Target.Value)), args.Used, args.User); // TODO: For better or worse, this will play a sound, but it's the // more future-proof thing to do than copying the same code // that OnBallisticInteractUsing has, sans sound. SimulateInsertAmmo(ent.Value, args.Used, Transform(args.Used).Coordinates); } else { SimulateInsertAmmo(ent.Value, args.Target.Value, Transform(args.Target.Value).Coordinates); } if (ent.Value.IsClientSide()) Del(ent.Value); } } private void OnBallisticVerb(EntityUid uid, BallisticAmmoProviderComponent component, GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract || args.Hands == null) return; args.Verbs.Add(new Verb() { Text = Loc.GetString("gun-ballistic-cycle"), Disabled = GetBallisticShots(component) == 0, Act = () => ManualCycle(uid, component, Transform(uid).MapPosition, 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) { // Reset shotting for cycling if (Resolve(uid, ref gunComp, false) && gunComp is { FireRate: > 0f } && !Paused(uid)) { gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRate); } Dirty(component); Audio.PlayPredicted(component.SoundRack, uid, user); var shots = GetBallisticShots(component); component.Cycled = true; 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"); } 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.FillProto != null) { component.UnspawnedCount = Math.Max(0, component.Capacity - component.Container.ContainedEntities.Count); Dirty(component); } } 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++) { if (!component.Cycled) break; EntityUid entity; if (component.Entities.Count > 0) { entity = component.Entities[^1]; args.Ammo.Add((entity, EnsureComp(entity))); // Leave the entity as is if it doesn't auto cycle // TODO: Suss this out with NewAmmoComponent as I don't think it gets removed from container properly if (!component.AutoCycle) { return; } component.Entities.RemoveAt(component.Entities.Count - 1); component.Container.Remove(entity); } else if (component.UnspawnedCount > 0) { component.UnspawnedCount--; entity = Spawn(component.FillProto, args.Coordinates); args.Ammo.Add((entity, EnsureComp(entity))); // Put it back in if it doesn't auto-cycle if (HasComp(entity) && !component.AutoCycle) { if (!entity.IsClientSide()) { component.Entities.Add(entity); component.Container.Insert(entity); } else { component.UnspawnedCount++; } } } if (!component.AutoCycle) { component.Cycled = false; } } UpdateBallisticAppearance(uid, component); Dirty(component); } private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, ref GetAmmoCountEvent args) { args.Count = GetBallisticShots(component); args.Capacity = component.Capacity; } private 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); } }