using System; using System.Threading.Tasks; using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition; using Content.Shared.GameObjects; using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.Verbs; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Players; using Robust.Shared.Random; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels { [RegisterComponent] public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks { [Dependency] private readonly IRobustRandom _random = default!; public override string Name => "RevolverBarrel"; public override uint? NetID => ContentNetIDs.REVOLVER_BARREL; [ViewVariables] [DataField("caliber")] private BallisticCaliber _caliber = BallisticCaliber.Unspecified; private Container _ammoContainer = default!; [ViewVariables] private int _currentSlot; public override int Capacity => _ammoSlots.Length; [DataField("capacity")] private int _serializedCapacity = 6; [DataField("ammoSlots", readOnly: true)] private IEntity?[] _ammoSlots = Array.Empty(); public override int ShotsLeft => _ammoContainer.ContainedEntities.Count; [ViewVariables] [DataField("fillPrototype")] private string? _fillPrototype; [ViewVariables] private int _unspawnedCount; // Sounds [DataField("soundEject")] private string _soundEject = "/Audio/Weapons/Guns/MagOut/revolver_magout.ogg"; [DataField("soundInsert")] private string _soundInsert = "/Audio/Weapons/Guns/MagIn/revolver_magin.ogg"; [DataField("soundSpin")] private string _soundSpin = "/Audio/Weapons/Guns/Misc/revolver_spin.ogg"; void ISerializationHooks.BeforeSerialization() { _serializedCapacity = _ammoSlots.Length; } void ISerializationHooks.AfterDeserialization() { _ammoSlots = new IEntity[_serializedCapacity]; } public override ComponentState GetComponentState(ICommonSession player) { var slotsSpent = new bool?[Capacity]; for (var i = 0; i < Capacity; i++) { slotsSpent[i] = null; var ammoEntity = _ammoSlots[i]; if (ammoEntity != null && ammoEntity.TryGetComponent(out AmmoComponent? ammo)) { slotsSpent[i] = ammo.Spent; } } //TODO: make yaml var to not sent currentSlot/UI? (for russian roulette) return new RevolverBarrelComponentState( _currentSlot, FireRateSelector, slotsSpent, SoundGunshot); } public override void Initialize() { base.Initialize(); _unspawnedCount = Capacity; int idx = 0; _ammoContainer = ContainerHelpers.EnsureContainer(Owner, $"{Name}-ammoContainer", out var existing); if (existing) { foreach (var entity in _ammoContainer.ContainedEntities) { _unspawnedCount--; _ammoSlots[idx] = entity; idx++; } } for (var i = 0; i < _unspawnedCount; i++) { var entity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.Coordinates); _ammoSlots[idx] = entity; _ammoContainer.Insert(entity); idx++; } UpdateAppearance(); Dirty(); } private void UpdateAppearance() { if (!Owner.TryGetComponent(out AppearanceComponent? appearance)) { return; } // Placeholder, at this stage it's just here for the RPG appearance.SetData(MagazineBarrelVisuals.MagLoaded, ShotsLeft > 0); appearance.SetData(AmmoVisuals.AmmoCount, ShotsLeft); appearance.SetData(AmmoVisuals.AmmoMax, Capacity); } public bool TryInsertBullet(IEntity user, IEntity entity) { if (!entity.TryGetComponent(out AmmoComponent? ammoComponent)) { return false; } if (ammoComponent.Caliber != _caliber) { Owner.PopupMessage(user, Loc.GetString("Wrong caliber")); return false; } // Functions like a stack // These are inserted in reverse order but then when fired Cycle will go through in order // The reason we don't just use an actual stack is because spin can select a random slot to point at for (var i = _ammoSlots.Length - 1; i >= 0; i--) { var slot = _ammoSlots[i]; if (slot == null) { _currentSlot = i; _ammoSlots[i] = entity; _ammoContainer.Insert(entity); if (_soundInsert != null) { SoundSystem.Play(Filter.Pvs(Owner), _soundInsert, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2)); } Dirty(); UpdateAppearance(); return true; } } Owner.PopupMessage(user, Loc.GetString("Ammo full")); return false; } public void Cycle() { // Move up a slot _currentSlot = (_currentSlot + 1) % _ammoSlots.Length; Dirty(); UpdateAppearance(); } /// /// Russian Roulette /// public void Spin() { var random = _random.Next(_ammoSlots.Length - 1); _currentSlot = random; if (!string.IsNullOrEmpty(_soundSpin)) { SoundSystem.Play(Filter.Pvs(Owner), _soundSpin, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2)); } Dirty(); } public override IEntity? PeekAmmo() { return _ammoSlots[_currentSlot]; } /// /// Takes a projectile out if possible /// IEnumerable just to make supporting shotguns saner /// /// /// public override IEntity? TakeProjectile(EntityCoordinates spawnAt) { var ammo = _ammoSlots[_currentSlot]; IEntity? bullet = null; if (ammo != null) { var ammoComponent = ammo.GetComponent(); bullet = ammoComponent.TakeBullet(spawnAt); if (ammoComponent.Caseless) { _ammoSlots[_currentSlot] = null; _ammoContainer.Remove(ammo); } } Cycle(); UpdateAppearance(); return bullet; } private void EjectAllSlots() { for (var i = 0; i < _ammoSlots.Length; i++) { var entity = _ammoSlots[i]; if (entity == null) { continue; } _ammoContainer.Remove(entity); EjectCasing(entity); _ammoSlots[i] = null; } if (_ammoContainer.ContainedEntities.Count > 0) { if (_soundEject != null) { SoundSystem.Play(Filter.Pvs(Owner), _soundEject, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-1)); } } // May as well point back at the end? _currentSlot = _ammoSlots.Length - 1; return; } /// /// Eject all casings /// /// /// /// public override bool UseEntity(UseEntityEventArgs eventArgs) { EjectAllSlots(); Dirty(); UpdateAppearance(); return true; } public override async Task InteractUsing(InteractUsingEventArgs eventArgs) { return TryInsertBullet(eventArgs.User, eventArgs.Using); } [Verb] private sealed class SpinRevolverVerb : Verb { protected override void GetData(IEntity user, RevolverBarrelComponent component, VerbData data) { if (!ActionBlockerSystem.CanInteract(user)) { data.Visibility = VerbVisibility.Invisible; return; } data.Text = Loc.GetString("Spin"); if (component.Capacity <= 1) { data.Visibility = VerbVisibility.Invisible; return; } data.Visibility = component.ShotsLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled; data.IconTexture = "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png"; } protected override void Activate(IEntity user, RevolverBarrelComponent component) { component.Spin(); component.Owner.PopupMessage(user, Loc.GetString("Spun the cylinder")); } } } }