using System; using System.Collections.Generic; using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Hands.Components; using Content.Server.PowerCell; using Content.Server.Stunnable; using Content.Server.Weapon.Ranged.Ammunition.Components; using Content.Server.Weapon.Ranged.Barrels.Components; using Content.Shared.ActionBlocker; using Content.Shared.Camera; using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Popups; using Content.Shared.PowerCell.Components; using Content.Shared.Verbs; using Content.Shared.Weapons.Ranged.Components; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server.Weapon.Ranged; public sealed partial class GunSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly AdminLogSystem _logs = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!; [Dependency] private readonly CameraRecoilSystem _recoil = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly EffectSystem _effects = default!; [Dependency] private readonly PowerCellSystem _cell = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; /// /// How many sounds are allowed to be played on ejecting multiple casings. /// private const int EjectionSoundMax = 3; public override void Initialize() { base.Initialize(); // TODO: So at the time I thought there might've been a need to keep magazines // and ammo boxes separate. // There isn't. // They should be combined. SubscribeLocalEvent(OnAmmoExamine); SubscribeLocalEvent(OnAmmoBoxInit); SubscribeLocalEvent(OnAmmoBoxMapInit); SubscribeLocalEvent(OnAmmoBoxExamine); SubscribeLocalEvent(OnAmmoBoxInteractUsing); SubscribeLocalEvent(OnAmmoBoxUse); SubscribeLocalEvent(OnAmmoBoxInteractHand); SubscribeLocalEvent>(OnAmmoBoxAltVerbs); SubscribeLocalEvent(OnRangedMagInit); SubscribeLocalEvent(OnRangedMagMapInit); SubscribeLocalEvent(OnRangedMagUse); SubscribeLocalEvent(OnRangedMagExamine); SubscribeLocalEvent(OnRangedMagInteractUsing); // Whenever I get around to refactoring guns this is all going to change. // Essentially the idea is // You have GunComponent and ChamberedGunComponent (which is just guncomp + containerslot for chamber) // GunComponent has a component for an ammo provider on it (e.g. battery) and asks it for ammo to shoot // ALTERNATIVELY, it has a "MagazineAmmoProvider" that has its own containerslot that it can ask // (All of these would be comp references so max you only ever have 2 components on the gun). SubscribeLocalEvent(OnBatteryInit); SubscribeLocalEvent(OnBatteryMapInit); SubscribeLocalEvent(OnCellSlotUpdated); SubscribeLocalEvent(OnBoltInit); SubscribeLocalEvent(OnBoltMapInit); SubscribeLocalEvent(OnBoltFireAttempt); SubscribeLocalEvent(OnBoltUse); SubscribeLocalEvent(OnBoltInteractUsing); SubscribeLocalEvent(OnBoltGetState); SubscribeLocalEvent(OnBoltExamine); SubscribeLocalEvent>(AddToggleBoltVerb); SubscribeLocalEvent(OnMagazineInit); SubscribeLocalEvent(OnMagazineMapInit); SubscribeLocalEvent(OnMagazineExamine); SubscribeLocalEvent(OnMagazineUse); SubscribeLocalEvent(OnMagazineInteractUsing); SubscribeLocalEvent(OnMagazineGetState); SubscribeLocalEvent>(AddMagazineInteractionVerbs); SubscribeLocalEvent>(AddEjectMagazineVerb); SubscribeLocalEvent(OnPumpGetState); SubscribeLocalEvent(OnPumpInit); SubscribeLocalEvent(OnPumpMapInit); SubscribeLocalEvent(OnPumpExamine); SubscribeLocalEvent(OnPumpUse); SubscribeLocalEvent(OnPumpInteractUsing); SubscribeLocalEvent(OnRevolverMapInit); SubscribeLocalEvent(OnRevolverUse); SubscribeLocalEvent(OnRevolverInteractUsing); SubscribeLocalEvent(OnRevolverGetState); SubscribeLocalEvent>(AddSpinVerb); SubscribeLocalEvent(OnSpeedLoaderInit); SubscribeLocalEvent(OnSpeedLoaderMapInit); SubscribeLocalEvent(OnSpeedLoaderUse); SubscribeLocalEvent(OnSpeedLoaderAfterInteract); SubscribeLocalEvent(OnSpeedLoaderInteractUsing); // SubscribeLocalEvent(OnGunExamine); SubscribeNetworkEvent(OnFirePos); } private void OnFirePos(FirePosEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not {Valid: true} user) return; if (!msg.Coordinates.IsValid(EntityManager)) return; if (!TryComp(user, out HandsComponent? handsComponent)) return; // TODO: Not exactly robust var gun = handsComponent.ActiveHand?.HeldEntity; if (gun == null || !TryComp(gun, out ServerRangedWeaponComponent? weapon)) return; // map pos TryFire(user, msg.Coordinates, weapon); } public EntityUid? PeekAtAmmo(ServerRangedBarrelComponent component) { return component switch { BatteryBarrelComponent battery => PeekAmmo(battery), BoltActionBarrelComponent bolt => PeekAmmo(bolt), MagazineBarrelComponent mag => PeekAmmo(mag), PumpBarrelComponent pump => PeekAmmo(pump), RevolverBarrelComponent revolver => PeekAmmo(revolver), _ => throw new NotImplementedException() }; } public EntityUid? TakeOutProjectile(ServerRangedBarrelComponent component, EntityCoordinates spawnAt) { return component switch { BatteryBarrelComponent battery => TakeProjectile(battery, spawnAt), BoltActionBarrelComponent bolt => TakeProjectile(bolt, spawnAt), MagazineBarrelComponent mag => TakeProjectile(mag, spawnAt), PumpBarrelComponent pump => TakeProjectile(pump, spawnAt), RevolverBarrelComponent revolver => TakeProjectile(revolver, spawnAt), _ => throw new NotImplementedException() }; } /// /// Drops multiple cartridges / shells on the floor /// Wraps EjectCasing to make it less toxic for bulk ejections /// public void EjectCasings(IEnumerable entities) { var soundPlayCount = 0; var playSound = true; foreach (var entity in entities) { EjectCasing(entity, playSound); soundPlayCount++; if (soundPlayCount > EjectionSoundMax) { playSound = false; } } } /// /// Drops a single cartridge / shell /// public void EjectCasing( EntityUid entity, bool playSound = true, AmmoComponent? ammoComponent = null) { const float ejectOffset = 0.4f; if (!Resolve(entity, ref ammoComponent)) return; var offsetPos = (_random.NextFloat(-ejectOffset, ejectOffset), _random.NextFloat(-ejectOffset, ejectOffset)); var xform = Transform(entity); var coordinates = xform.Coordinates; coordinates = coordinates.Offset(offsetPos); xform.LocalRotation = _random.NextFloat(MathF.Tau); xform.Coordinates = coordinates; if (playSound) SoundSystem.Play(Filter.Pvs(entity), ammoComponent.SoundCollectionEject.GetSound(), coordinates, AudioParams.Default.WithVolume(-1)); } private Angle GetRecoilAngle(ServerRangedBarrelComponent component, Angle direction) { var currentTime = _gameTiming.CurTime; var timeSinceLastFire = (currentTime - component.LastFire).TotalSeconds; var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease - component.AngleDecay * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta); component.CurrentAngle = new Angle(newTheta); var random = (_random.NextDouble(-1, 1)); var angle = Angle.FromDegrees(direction.Degrees + component.CurrentAngle.Degrees * random); return angle; } /// /// Raised on a gun when it fires. /// public sealed class GunShotEvent : EntityEventArgs { } public sealed class GunFireAttemptEvent : CancellableEntityEventArgs { public EntityUid? User = null; public ServerRangedWeaponComponent Weapon; public GunFireAttemptEvent(EntityUid? user, ServerRangedWeaponComponent weapon) { User = user; Weapon = weapon; } } }