using System; using System.Collections.Generic; using Content.Server.GameObjects.EntitySystems; using Content.Server.Utility; using Content.Shared.GameObjects.Components.Weapons.Ranged; using Content.Shared.Interfaces; using Robust.Server.GameObjects; using Robust.Server.GameObjects.Components.Container; using Robust.Server.Interfaces.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile { [RegisterComponent] public class BallisticMagazineComponent : Component, IMapInit, IAttackBy { public override string Name => "BallisticMagazine"; // Stack of loaded bullets. [ViewVariables] private readonly Stack _loadedBullets = new Stack(); private string _fillType; [ViewVariables] private Container _bulletContainer; [ViewVariables] private AppearanceComponent _appearance; private BallisticMagazineType _magazineType; private BallisticCaliber _caliber; private int _capacity; [ViewVariables] public string FillType => _fillType; [ViewVariables] public BallisticMagazineType MagazineType => _magazineType; [ViewVariables] public BallisticCaliber Caliber => _caliber; [ViewVariables] public int Capacity => _capacity; [ViewVariables] public int CountLoaded => _loadedBullets.Count + _availableSpawnCount; [ViewVariables] private int _availableSpawnCount; public event Action OnAmmoCountChanged; public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); serializer.DataField(ref _magazineType, "magazine", BallisticMagazineType.Unspecified); serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified); serializer.DataField(ref _fillType, "fill", null); serializer.DataField(ref _capacity, "capacity", 20); serializer.DataField(ref _availableSpawnCount, "availableSpawnCount", Capacity); } public override void Initialize() { base.Initialize(); _appearance = Owner.GetComponent(); } /// protected override void Startup() { base.Startup(); _bulletContainer = ContainerManagerComponent.Ensure("magazine_bullet_container", Owner, out var existed); if (existed) { foreach (var entity in _bulletContainer.ContainedEntities) { _loadedBullets.Push(entity); } } UpdateAppearance(); OnAmmoCountChanged?.Invoke(); _appearance.SetData(BallisticMagazineVisuals.AmmoCapacity, Capacity); } public void AddBullet(IEntity bullet) { if (!bullet.TryGetComponent(out BallisticBulletComponent component)) { throw new ArgumentException("entity isn't a bullet.", nameof(bullet)); } if (component.Caliber != Caliber) { throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet)); } if (CountLoaded >= Capacity) { throw new InvalidOperationException("Magazine is full."); } _bulletContainer.Insert(bullet); _loadedBullets.Push(bullet); UpdateAppearance(); OnAmmoCountChanged?.Invoke(); } public IEntity TakeBullet() { IEntity bullet; if (_loadedBullets.Count == 0) { if (_availableSpawnCount == 0) { return null; } _availableSpawnCount -= 1; bullet = Owner.EntityManager.SpawnEntity(FillType, Owner.Transform.GridPosition); } else { bullet = _loadedBullets.Pop(); _bulletContainer.Remove(bullet); } UpdateAppearance(); OnAmmoCountChanged?.Invoke(); return bullet; } // TODO: Allow putting individual casings into mag (also box) AmmoMagTransferPopupMessage CanTransferFrom(IEntity source) { // Currently the below duplicates box but at some stage these will likely differ if (source.TryGetComponent(out BallisticMagazineComponent magazineComponent)) { if (magazineComponent.Caliber != Caliber) { return new AmmoMagTransferPopupMessage(result: false, message: "Wrong caliber"); } if (CountLoaded == Capacity) { return new AmmoMagTransferPopupMessage(result: false, message: "Already full"); } if (magazineComponent.CountLoaded == 0) { return new AmmoMagTransferPopupMessage(result: false, message: "No ammo to transfer"); } return new AmmoMagTransferPopupMessage(result: true, message: ""); } // If box if (source.TryGetComponent(out AmmoBoxComponent boxComponent)) { if (boxComponent.Caliber != Caliber) { return new AmmoMagTransferPopupMessage(result: false, message: "Wrong caliber"); } if (CountLoaded == Capacity) { return new AmmoMagTransferPopupMessage(result: false, message: "Already full"); } if (boxComponent.CountLeft == 0) { return new AmmoMagTransferPopupMessage(result: false, message: "No ammo to transfer"); } return new AmmoMagTransferPopupMessage(result: true, message: ""); } return new AmmoMagTransferPopupMessage(result: false, message: ""); } // TODO: Potentially abstract out to reduce duplicate structs private struct AmmoMagTransferPopupMessage { public readonly bool Result; public readonly string Message; public AmmoMagTransferPopupMessage(bool result, string message) { Result = result; Message = message; } } bool IAttackBy.AttackBy(AttackByEventArgs eventArgs) { var ammoMagTransfer = CanTransferFrom(eventArgs.AttackWith); if (ammoMagTransfer.Result) { IEntity bullet; if (eventArgs.AttackWith.TryGetComponent(out BallisticMagazineComponent magazineComponent)) { int fillCount = Math.Min(magazineComponent.CountLoaded, Capacity - CountLoaded); for (int i = 0; i < fillCount; i++) { bullet = magazineComponent.TakeBullet(); AddBullet(bullet); } eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds"); return true; } if (eventArgs.AttackWith.TryGetComponent(out AmmoBoxComponent boxComponent)) { int fillCount = Math.Min(boxComponent.CountLeft, Capacity - CountLoaded); for (int i = 0; i < fillCount; i++) { bullet = boxComponent.TakeBullet(); AddBullet(bullet); } eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds"); return true; } } else { eventArgs.User.PopupMessage(eventArgs.User, ammoMagTransfer.Message); } return false; } private void UpdateAppearance() { _appearance.SetData(BallisticMagazineVisuals.AmmoLeft, CountLoaded); } public void MapInit() { _availableSpawnCount = Capacity; } } public enum BallisticMagazineType { Unspecified = 0, // .32 A32, // .357 A357, // .44 A44, // .45mm A45mm, // .50 cal A50, // 5.56mm A556mm, // 6.5mm A65mm, // 7.62mm A762mm, Maxim, // 9mm A9mm, A9mmSMG, A9mmTopMounted, // 10mm A10mm, A10mmSMG, // 20mm A20mm, // 24mm A24mm, // 12g A12g, } }