using Content.Shared.Examine; using Content.Shared.Interaction; 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; using Robust.Shared.Utility; namespace Content.Shared.Weapons.Ranged.Systems; public abstract partial class SharedGunSystem { protected virtual void InitializeBallistic() { SubscribeLocalEvent(OnBallisticInit); SubscribeLocalEvent(OnBallisticMapInit); SubscribeLocalEvent(OnBallisticTakeAmmo); SubscribeLocalEvent(OnBallisticAmmoCount); SubscribeLocalEvent(OnBallisticGetState); SubscribeLocalEvent(OnBallisticHandleState); SubscribeLocalEvent(OnBallisticExamine); SubscribeLocalEvent>(OnBallisticVerb); SubscribeLocalEvent(OnBallisticInteractUsing); SubscribeLocalEvent(OnBallisticActivate); } private void OnBallisticActivate(EntityUid uid, BallisticAmmoProviderComponent component, ActivateInWorldEvent args) { ManualCycle(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 PlaySound(uid, component.SoundInsert?.GetSound(Random, ProtoManager), args.User); args.Handled = true; UpdateBallisticAppearance(component); Dirty(component); } 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(component, Transform(uid).MapPosition, args.User), }); } private void OnBallisticExamine(EntityUid uid, BallisticAmmoProviderComponent component, ExaminedEvent args) { args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", GetBallisticShots(component)))); } private void ManualCycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates, EntityUid? user = null) { // Reset shotting for cycling if (TryComp(component.Owner, out var gunComp) && gunComp is { FireRate: > 0f }) { gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRate); } Dirty(component); var sound = component.SoundRack?.GetSound(Random, ProtoManager); if (sound != null) PlaySound(component.Owner, sound, user); var shots = GetBallisticShots(component); component.Cycled = true; Cycle(component, coordinates); var text = Loc.GetString(shots == 0 ? "gun-ballistic-cycled-empty" : "gun-ballistic-cycled"); Popup(text, component.Owner, user); UpdateBallisticAppearance(component); UpdateAmmoCount(component.Owner); } protected abstract void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates); private void OnBallisticGetState(EntityUid uid, BallisticAmmoProviderComponent component, ref ComponentGetState args) { args.State = new BallisticAmmoProviderComponentState() { UnspawnedCount = component.UnspawnedCount, Entities = component.Entities, Cycled = component.Cycled, }; } private void OnBallisticHandleState(EntityUid uid, BallisticAmmoProviderComponent component, ref ComponentHandleState args) { if (args.Current is not BallisticAmmoProviderComponentState state) return; component.Cycled = state.Cycled; component.UnspawnedCount = state.UnspawnedCount; component.Entities.Clear(); foreach (var ent in state.Entities) { component.Entities.Add(ent); } } 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) { if (component.FillProto != null) component.UnspawnedCount -= Math.Min(component.UnspawnedCount, component.Container.ContainedEntities.Count); } 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(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(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(component); Dirty(component); } private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, ref GetAmmoCountEvent args) { args.Count = GetBallisticShots(component); args.Capacity = component.Capacity; } private void UpdateBallisticAppearance(BallisticAmmoProviderComponent component) { if (!Timing.IsFirstTimePredicted || !TryComp(component.Owner, out var appearance)) return; appearance.SetData(AmmoVisuals.AmmoCount, GetBallisticShots(component)); appearance.SetData(AmmoVisuals.AmmoMax, component.Capacity); } [Serializable, NetSerializable] private sealed class BallisticAmmoProviderComponentState : ComponentState { public int UnspawnedCount; public List Entities = default!; public bool Cycled; } }