SharedGunSystem.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Numerics;
  3. using Content.Shared.ActionBlocker;
  4. using Content.Shared.Actions;
  5. using Content.Shared.Administration.Logs;
  6. using Content.Shared.Audio;
  7. using Content.Shared.CombatMode;
  8. using Content.Shared.Containers.ItemSlots;
  9. using Content.Shared.Damage;
  10. using Content.Shared.Examine;
  11. using Content.Shared.Gravity;
  12. using Content.Shared.Hands;
  13. using Content.Shared.Hands.Components;
  14. using Content.Shared.Popups;
  15. using Content.Shared.Projectiles;
  16. using Content.Shared.Tag;
  17. using Content.Shared.Throwing;
  18. using Content.Shared.Timing;
  19. using Content.Shared.Verbs;
  20. using Content.Shared.Weapons.Melee;
  21. using Content.Shared.Weapons.Melee.Events;
  22. using Content.Shared.Weapons.Ranged.Components;
  23. using Content.Shared.Weapons.Ranged.Events;
  24. using Content.Shared.Whitelist;
  25. using Robust.Shared.Audio;
  26. using Robust.Shared.Audio.Systems;
  27. using Robust.Shared.Containers;
  28. using Robust.Shared.Map;
  29. using Robust.Shared.Network;
  30. using Robust.Shared.Physics.Components;
  31. using Robust.Shared.Physics.Systems;
  32. using Robust.Shared.Prototypes;
  33. using Robust.Shared.Random;
  34. using Robust.Shared.Serialization;
  35. using Robust.Shared.Timing;
  36. using Robust.Shared.Utility;
  37. namespace Content.Shared.Weapons.Ranged.Systems;
  38. public abstract partial class SharedGunSystem : EntitySystem
  39. {
  40. [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
  41. [Dependency] protected readonly IGameTiming Timing = default!;
  42. [Dependency] protected readonly IMapManager MapManager = default!;
  43. [Dependency] private readonly INetManager _netManager = default!;
  44. [Dependency] protected readonly IPrototypeManager ProtoManager = default!;
  45. [Dependency] protected readonly IRobustRandom Random = default!;
  46. [Dependency] protected readonly ISharedAdminLogManager Logs = default!;
  47. [Dependency] protected readonly DamageableSystem Damageable = default!;
  48. [Dependency] protected readonly ExamineSystemShared Examine = default!;
  49. [Dependency] private readonly ItemSlotsSystem _slots = default!;
  50. [Dependency] private readonly RechargeBasicEntityAmmoSystem _recharge = default!;
  51. [Dependency] protected readonly SharedActionsSystem Actions = default!;
  52. [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
  53. [Dependency] protected readonly SharedAudioSystem Audio = default!;
  54. [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
  55. [Dependency] protected readonly SharedContainerSystem Containers = default!;
  56. [Dependency] private readonly SharedGravitySystem _gravity = default!;
  57. [Dependency] protected readonly SharedPointLightSystem Lights = default!;
  58. [Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
  59. [Dependency] protected readonly SharedPhysicsSystem Physics = default!;
  60. [Dependency] protected readonly SharedProjectileSystem Projectiles = default!;
  61. [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
  62. [Dependency] protected readonly TagSystem TagSystem = default!;
  63. [Dependency] protected readonly ThrowingSystem ThrowingSystem = default!;
  64. [Dependency] private readonly UseDelaySystem _useDelay = default!;
  65. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  66. private const float InteractNextFire = 0.3f;
  67. private const double SafetyNextFire = 0.5;
  68. private const float EjectOffset = 0.4f;
  69. protected const string AmmoExamineColor = "yellow";
  70. protected const string FireRateExamineColor = "yellow";
  71. public const string ModeExamineColor = "cyan";
  72. public override void Initialize()
  73. {
  74. SubscribeAllEvent<RequestShootEvent>(OnShootRequest);
  75. SubscribeAllEvent<RequestStopShootEvent>(OnStopShootRequest);
  76. SubscribeLocalEvent<GunComponent, MeleeHitEvent>(OnGunMelee);
  77. // Ammo providers
  78. InitializeBallistic();
  79. InitializeBattery();
  80. InitializeCartridge();
  81. InitializeChamberMagazine();
  82. InitializeMagazine();
  83. InitializeRevolver();
  84. InitializeBasicEntity();
  85. InitializeClothing();
  86. InitializeContainer();
  87. InitializeSolution();
  88. // Interactions
  89. SubscribeLocalEvent<GunComponent, GetVerbsEvent<AlternativeVerb>>(OnAltVerb);
  90. SubscribeLocalEvent<GunComponent, ExaminedEvent>(OnExamine);
  91. SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode);
  92. SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected);
  93. SubscribeLocalEvent<GunComponent, MapInitEvent>(OnMapInit);
  94. }
  95. private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args)
  96. {
  97. #if DEBUG
  98. if (gun.Comp.NextFire > Timing.CurTime)
  99. Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(gun)}");
  100. DebugTools.Assert((gun.Comp.AvailableModes & gun.Comp.SelectedMode) != 0x0);
  101. #endif
  102. RefreshModifiers((gun, gun));
  103. }
  104. private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args)
  105. {
  106. if (!TryComp<MeleeWeaponComponent>(uid, out var melee))
  107. return;
  108. if (melee.NextAttack > component.NextFire)
  109. {
  110. component.NextFire = melee.NextAttack;
  111. EntityManager.DirtyField(uid, component, nameof(GunComponent.NextFire));
  112. }
  113. }
  114. private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args)
  115. {
  116. var user = args.SenderSession.AttachedEntity;
  117. if (user == null ||
  118. !_combatMode.IsInCombatMode(user) ||
  119. !TryGetGun(user.Value, out var ent, out var gun))
  120. {
  121. return;
  122. }
  123. if (ent != GetEntity(msg.Gun))
  124. return;
  125. gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
  126. gun.Target = GetEntity(msg.Target);
  127. AttemptShoot(user.Value, ent, gun);
  128. }
  129. private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
  130. {
  131. var gunUid = GetEntity(ev.Gun);
  132. if (args.SenderSession.AttachedEntity == null ||
  133. !TryComp<GunComponent>(gunUid, out var gun) ||
  134. !TryGetGun(args.SenderSession.AttachedEntity.Value, out _, out var userGun))
  135. {
  136. return;
  137. }
  138. if (userGun != gun)
  139. return;
  140. StopShooting(gunUid, gun);
  141. }
  142. public bool CanShoot(GunComponent component)
  143. {
  144. if (component.NextFire > Timing.CurTime)
  145. return false;
  146. return true;
  147. }
  148. public bool TryGetGun(EntityUid entity, out EntityUid gunEntity, [NotNullWhen(true)] out GunComponent? gunComp)
  149. {
  150. gunEntity = default;
  151. gunComp = null;
  152. if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) &&
  153. hands.ActiveHandEntity is { } held &&
  154. TryComp(held, out GunComponent? gun))
  155. {
  156. gunEntity = held;
  157. gunComp = gun;
  158. return true;
  159. }
  160. // Last resort is check if the entity itself is a gun.
  161. if (TryComp(entity, out gun))
  162. {
  163. gunEntity = entity;
  164. gunComp = gun;
  165. return true;
  166. }
  167. return false;
  168. }
  169. private void StopShooting(EntityUid uid, GunComponent gun)
  170. {
  171. if (gun.ShotCounter == 0)
  172. return;
  173. gun.ShotCounter = 0;
  174. gun.ShootCoordinates = null;
  175. gun.Target = null;
  176. EntityManager.DirtyField(uid, gun, nameof(GunComponent.ShotCounter));
  177. }
  178. /// <summary>
  179. /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
  180. /// </summary>
  181. public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates)
  182. {
  183. gun.ShootCoordinates = toCoordinates;
  184. AttemptShoot(user, gunUid, gun);
  185. gun.ShotCounter = 0;
  186. EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
  187. }
  188. /// <summary>
  189. /// Shoots by assuming the gun is the user at default coordinates.
  190. /// </summary>
  191. public void AttemptShoot(EntityUid gunUid, GunComponent gun)
  192. {
  193. var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection);
  194. gun.ShootCoordinates = coordinates;
  195. AttemptShoot(gunUid, gunUid, gun);
  196. gun.ShotCounter = 0;
  197. }
  198. private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
  199. {
  200. if (gun.FireRateModified <= 0f ||
  201. !_actionBlockerSystem.CanAttack(user))
  202. {
  203. return;
  204. }
  205. var toCoordinates = gun.ShootCoordinates;
  206. if (toCoordinates == null)
  207. return;
  208. var curTime = Timing.CurTime;
  209. // check if anything wants to prevent shooting
  210. var prevention = new ShotAttemptedEvent
  211. {
  212. User = user,
  213. Used = (gunUid, gun)
  214. };
  215. RaiseLocalEvent(gunUid, ref prevention);
  216. if (prevention.Cancelled)
  217. return;
  218. RaiseLocalEvent(user, ref prevention);
  219. if (prevention.Cancelled)
  220. return;
  221. // Need to do this to play the clicking sound for empty automatic weapons
  222. // but not play anything for burst fire.
  223. if (gun.NextFire > curTime)
  224. return;
  225. var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
  226. if (gun.SelectedMode == SelectiveFire.Burst || gun.BurstActivated)
  227. fireRate = TimeSpan.FromSeconds(1f / gun.BurstFireRate);
  228. // First shot
  229. // Previously we checked shotcounter but in some cases all the bullets got dumped at once
  230. // curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker.
  231. if (gun.NextFire < curTime - fireRate || gun.ShotCounter == 0 && gun.NextFire < curTime)
  232. gun.NextFire = curTime;
  233. var shots = 0;
  234. var lastFire = gun.NextFire;
  235. while (gun.NextFire <= curTime)
  236. {
  237. gun.NextFire += fireRate;
  238. shots++;
  239. }
  240. // NextFire has been touched regardless so need to dirty the gun.
  241. EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.NextFire));
  242. // Get how many shots we're actually allowed to make, due to clip size or otherwise.
  243. // Don't do this in the loop so we still reset NextFire.
  244. if (!gun.BurstActivated)
  245. {
  246. switch (gun.SelectedMode)
  247. {
  248. case SelectiveFire.SemiAuto:
  249. shots = Math.Min(shots, 1 - gun.ShotCounter);
  250. break;
  251. case SelectiveFire.Burst:
  252. shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
  253. break;
  254. case SelectiveFire.FullAuto:
  255. break;
  256. default:
  257. throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!");
  258. }
  259. } else
  260. {
  261. shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
  262. }
  263. var attemptEv = new AttemptShootEvent(user, null);
  264. RaiseLocalEvent(gunUid, ref attemptEv);
  265. if (attemptEv.Cancelled)
  266. {
  267. if (attemptEv.Message != null)
  268. {
  269. PopupSystem.PopupClient(attemptEv.Message, gunUid, user);
  270. }
  271. gun.BurstActivated = false;
  272. gun.BurstShotsCount = 0;
  273. gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
  274. return;
  275. }
  276. var fromCoordinates = Transform(user).Coordinates;
  277. // Remove ammo
  278. var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
  279. // Listen it just makes the other code around it easier if shots == 0 to do this.
  280. if (shots > 0)
  281. RaiseLocalEvent(gunUid, ev);
  282. DebugTools.Assert(ev.Ammo.Count <= shots);
  283. DebugTools.Assert(shots >= 0);
  284. UpdateAmmoCount(gunUid);
  285. // Even if we don't actually shoot update the ShotCounter. This is to avoid spamming empty sounds
  286. // where the gun may be SemiAuto or Burst.
  287. gun.ShotCounter += shots;
  288. EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
  289. if (ev.Ammo.Count <= 0)
  290. {
  291. // triggers effects on the gun if it's empty
  292. var emptyGunShotEvent = new OnEmptyGunShotEvent();
  293. RaiseLocalEvent(gunUid, ref emptyGunShotEvent);
  294. gun.BurstActivated = false;
  295. gun.BurstShotsCount = 0;
  296. gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
  297. // Play empty gun sounds if relevant
  298. // If they're firing an existing clip then don't play anything.
  299. if (shots > 0)
  300. {
  301. PopupSystem.PopupCursor(ev.Reason ?? Loc.GetString("gun-magazine-fired-empty"));
  302. // Don't spam safety sounds at gun fire rate, play it at a reduced rate.
  303. // May cause prediction issues? Needs more tweaking
  304. gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
  305. Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
  306. return;
  307. }
  308. return;
  309. }
  310. // Handle burstfire
  311. if (gun.SelectedMode == SelectiveFire.Burst)
  312. {
  313. gun.BurstActivated = true;
  314. }
  315. if (gun.BurstActivated)
  316. {
  317. gun.BurstShotsCount += shots;
  318. if (gun.BurstShotsCount >= gun.ShotsPerBurstModified)
  319. {
  320. gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
  321. gun.BurstActivated = false;
  322. gun.BurstShotsCount = 0;
  323. }
  324. }
  325. // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent).
  326. Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems);
  327. var shotEv = new GunShotEvent(user, ev.Ammo);
  328. RaiseLocalEvent(gunUid, ref shotEv);
  329. if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
  330. {
  331. if (_gravity.IsWeightless(user, userPhysics))
  332. CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
  333. }
  334. }
  335. public void Shoot(
  336. EntityUid gunUid,
  337. GunComponent gun,
  338. EntityUid ammo,
  339. EntityCoordinates fromCoordinates,
  340. EntityCoordinates toCoordinates,
  341. out bool userImpulse,
  342. EntityUid? user = null,
  343. bool throwItems = false)
  344. {
  345. var shootable = EnsureShootable(ammo);
  346. Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems);
  347. }
  348. public abstract void Shoot(
  349. EntityUid gunUid,
  350. GunComponent gun,
  351. List<(EntityUid? Entity, IShootable Shootable)> ammo,
  352. EntityCoordinates fromCoordinates,
  353. EntityCoordinates toCoordinates,
  354. out bool userImpulse,
  355. EntityUid? user = null,
  356. bool throwItems = false);
  357. public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocity, EntityUid gunUid, EntityUid? user = null, float speed = 20f)
  358. {
  359. var physics = EnsureComp<PhysicsComponent>(uid);
  360. Physics.SetBodyStatus(uid, physics, BodyStatus.InAir);
  361. var targetMapVelocity = gunVelocity + direction.Normalized() * speed;
  362. var currentMapVelocity = Physics.GetMapLinearVelocity(uid, physics);
  363. var finalLinear = physics.LinearVelocity + targetMapVelocity - currentMapVelocity;
  364. Physics.SetLinearVelocity(uid, finalLinear, body: physics);
  365. var projectile = EnsureComp<ProjectileComponent>(uid);
  366. Projectiles.SetShooter(uid, projectile, user ?? gunUid);
  367. projectile.Weapon = gunUid;
  368. TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle() + projectile.Angle);
  369. }
  370. protected abstract void Popup(string message, EntityUid? uid, EntityUid? user);
  371. /// <summary>
  372. /// Call this whenever the ammo count for a gun changes.
  373. /// </summary>
  374. protected virtual void UpdateAmmoCount(EntityUid uid, bool prediction = true) {}
  375. protected void SetCartridgeSpent(EntityUid uid, CartridgeAmmoComponent cartridge, bool spent)
  376. {
  377. if (cartridge.Spent != spent)
  378. DirtyField(uid, cartridge, nameof(CartridgeAmmoComponent.Spent));
  379. cartridge.Spent = spent;
  380. Appearance.SetData(uid, AmmoVisuals.Spent, spent);
  381. }
  382. /// <summary>
  383. /// Drops a single cartridge / shell
  384. /// </summary>
  385. protected void EjectCartridge(
  386. EntityUid entity,
  387. Angle? angle = null,
  388. bool playSound = true)
  389. {
  390. // TODO: Sound limit version.
  391. var offsetPos = Random.NextVector2(EjectOffset);
  392. var xform = Transform(entity);
  393. var coordinates = xform.Coordinates;
  394. coordinates = coordinates.Offset(offsetPos);
  395. TransformSystem.SetLocalRotation(xform, Random.NextAngle());
  396. TransformSystem.SetCoordinates(entity, xform, coordinates);
  397. // decides direction the casing ejects and only when not cycling
  398. if (angle != null)
  399. {
  400. Angle ejectAngle = angle.Value;
  401. ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun
  402. ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f);
  403. }
  404. if (playSound && TryComp<CartridgeAmmoComponent>(entity, out var cartridge))
  405. {
  406. Audio.PlayPvs(cartridge.EjectSound, entity, AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-1f));
  407. }
  408. }
  409. protected IShootable EnsureShootable(EntityUid uid)
  410. {
  411. if (TryComp<CartridgeAmmoComponent>(uid, out var cartridge))
  412. return cartridge;
  413. return EnsureComp<AmmoComponent>(uid);
  414. }
  415. protected void RemoveShootable(EntityUid uid)
  416. {
  417. RemCompDeferred<CartridgeAmmoComponent>(uid);
  418. RemCompDeferred<AmmoComponent>(uid);
  419. }
  420. protected void MuzzleFlash(EntityUid gun, AmmoComponent component, Angle worldAngle, EntityUid? user = null)
  421. {
  422. var attemptEv = new GunMuzzleFlashAttemptEvent();
  423. RaiseLocalEvent(gun, ref attemptEv);
  424. if (attemptEv.Cancelled)
  425. return;
  426. var sprite = component.MuzzleFlash;
  427. if (sprite == null)
  428. return;
  429. var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, worldAngle);
  430. CreateEffect(gun, ev, user);
  431. }
  432. public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics)
  433. {
  434. var fromMap = fromCoordinates.ToMapPos(EntityManager, TransformSystem);
  435. var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
  436. var shotDirection = (toMap - fromMap).Normalized();
  437. const float impulseStrength = 25.0f;
  438. var impulseVector = shotDirection * impulseStrength;
  439. Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics);
  440. }
  441. public void RefreshModifiers(Entity<GunComponent?> gun)
  442. {
  443. if (!Resolve(gun, ref gun.Comp))
  444. return;
  445. var comp = gun.Comp;
  446. var ev = new GunRefreshModifiersEvent(
  447. (gun, comp),
  448. comp.SoundGunshot,
  449. comp.CameraRecoilScalar,
  450. comp.AngleIncrease,
  451. comp.AngleDecay,
  452. comp.MaxAngle,
  453. comp.MinAngle,
  454. comp.ShotsPerBurst,
  455. comp.FireRate,
  456. comp.ProjectileSpeed
  457. );
  458. RaiseLocalEvent(gun, ref ev);
  459. if (comp.SoundGunshotModified != ev.SoundGunshot)
  460. {
  461. comp.SoundGunshotModified = ev.SoundGunshot;
  462. DirtyField(gun, nameof(GunComponent.SoundGunshotModified));
  463. }
  464. if (!MathHelper.CloseTo(comp.CameraRecoilScalarModified, ev.CameraRecoilScalar))
  465. {
  466. comp.CameraRecoilScalarModified = ev.CameraRecoilScalar;
  467. DirtyField(gun, nameof(GunComponent.CameraRecoilScalarModified));
  468. }
  469. if (!comp.AngleIncreaseModified.EqualsApprox(ev.AngleIncrease))
  470. {
  471. comp.AngleIncreaseModified = ev.AngleIncrease;
  472. DirtyField(gun, nameof(GunComponent.AngleIncreaseModified));
  473. }
  474. if (!comp.AngleDecayModified.EqualsApprox(ev.AngleDecay))
  475. {
  476. comp.AngleDecayModified = ev.AngleDecay;
  477. DirtyField(gun, nameof(GunComponent.AngleDecayModified));
  478. }
  479. if (!comp.MaxAngleModified.EqualsApprox(ev.MinAngle))
  480. {
  481. comp.MaxAngleModified = ev.MaxAngle;
  482. DirtyField(gun, nameof(GunComponent.MaxAngleModified));
  483. }
  484. if (!comp.MinAngleModified.EqualsApprox(ev.MinAngle))
  485. {
  486. comp.MinAngleModified = ev.MinAngle;
  487. DirtyField(gun, nameof(GunComponent.MinAngleModified));
  488. }
  489. if (comp.ShotsPerBurstModified != ev.ShotsPerBurst)
  490. {
  491. comp.ShotsPerBurstModified = ev.ShotsPerBurst;
  492. DirtyField(gun, nameof(GunComponent.ShotsPerBurstModified));
  493. }
  494. if (!MathHelper.CloseTo(comp.FireRateModified, ev.FireRate))
  495. {
  496. comp.FireRateModified = ev.FireRate;
  497. DirtyField(gun, nameof(GunComponent.FireRateModified));
  498. }
  499. if (!MathHelper.CloseTo(comp.ProjectileSpeedModified, ev.ProjectileSpeed))
  500. {
  501. comp.ProjectileSpeedModified = ev.ProjectileSpeed;
  502. DirtyField(gun, nameof(GunComponent.ProjectileSpeedModified));
  503. }
  504. }
  505. protected abstract void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null);
  506. /// <summary>
  507. /// Used for animated effects on the client.
  508. /// </summary>
  509. [Serializable, NetSerializable]
  510. public sealed class HitscanEvent : EntityEventArgs
  511. {
  512. public List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier Sprite, float Distance)> Sprites = new();
  513. }
  514. }
  515. /// <summary>
  516. /// Raised directed on the gun before firing to see if the shot should go through.
  517. /// </summary>
  518. /// <remarks>
  519. /// Handling this in server exclusively will lead to mispredicts.
  520. /// </remarks>
  521. /// <param name="User">The user that attempted to fire this gun.</param>
  522. /// <param name="Cancelled">Set this to true if the shot should be cancelled.</param>
  523. /// <param name="ThrowItems">Set this to true if the ammo shouldn't actually be fired, just thrown.</param>
  524. [ByRefEvent]
  525. public record struct AttemptShootEvent(EntityUid User, string? Message, bool Cancelled = false, bool ThrowItems = false);
  526. /// <summary>
  527. /// Raised directed on the gun after firing.
  528. /// </summary>
  529. /// <param name="User">The user that fired this gun.</param>
  530. [ByRefEvent]
  531. public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo);
  532. public enum EffectLayers : byte
  533. {
  534. Unshaded,
  535. }
  536. [Serializable, NetSerializable]
  537. public enum AmmoVisuals : byte
  538. {
  539. Spent,
  540. AmmoCount,
  541. AmmoMax,
  542. HasAmmo, // used for generic visualizers. c# stuff can just check ammocount != 0
  543. MagLoaded,
  544. BoltClosed,
  545. }