SharedGunSystem.Ballistic.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. using Content.Shared.DoAfter;
  2. using Content.Shared.Examine;
  3. using Content.Shared.Interaction;
  4. using Content.Shared.Interaction.Events;
  5. using Content.Shared.Verbs;
  6. using Content.Shared.Weapons.Ranged.Components;
  7. using Content.Shared.Weapons.Ranged.Events;
  8. using Robust.Shared.Containers;
  9. using Robust.Shared.Map;
  10. using Robust.Shared.Serialization;
  11. namespace Content.Shared.Weapons.Ranged.Systems;
  12. public abstract partial class SharedGunSystem
  13. {
  14. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  15. [Dependency] private readonly SharedInteractionSystem _interaction = default!;
  16. protected virtual void InitializeBallistic()
  17. {
  18. SubscribeLocalEvent<BallisticAmmoProviderComponent, ComponentInit>(OnBallisticInit);
  19. SubscribeLocalEvent<BallisticAmmoProviderComponent, MapInitEvent>(OnBallisticMapInit);
  20. SubscribeLocalEvent<BallisticAmmoProviderComponent, TakeAmmoEvent>(OnBallisticTakeAmmo);
  21. SubscribeLocalEvent<BallisticAmmoProviderComponent, GetAmmoCountEvent>(OnBallisticAmmoCount);
  22. SubscribeLocalEvent<BallisticAmmoProviderComponent, ExaminedEvent>(OnBallisticExamine);
  23. SubscribeLocalEvent<BallisticAmmoProviderComponent, GetVerbsEvent<Verb>>(OnBallisticVerb);
  24. SubscribeLocalEvent<BallisticAmmoProviderComponent, InteractUsingEvent>(OnBallisticInteractUsing);
  25. SubscribeLocalEvent<BallisticAmmoProviderComponent, AfterInteractEvent>(OnBallisticAfterInteract);
  26. SubscribeLocalEvent<BallisticAmmoProviderComponent, AmmoFillDoAfterEvent>(OnBallisticAmmoFillDoAfter);
  27. SubscribeLocalEvent<BallisticAmmoProviderComponent, UseInHandEvent>(OnBallisticUse);
  28. }
  29. private void OnBallisticUse(EntityUid uid, BallisticAmmoProviderComponent component, UseInHandEvent args)
  30. {
  31. if (args.Handled)
  32. return;
  33. ManualCycle(uid, component, TransformSystem.GetMapCoordinates(uid), args.User);
  34. args.Handled = true;
  35. }
  36. private void OnBallisticInteractUsing(EntityUid uid, BallisticAmmoProviderComponent component, InteractUsingEvent args)
  37. {
  38. if (args.Handled)
  39. return;
  40. if (_whitelistSystem.IsWhitelistFailOrNull(component.Whitelist, args.Used))
  41. return;
  42. if (GetBallisticShots(component) >= component.Capacity)
  43. return;
  44. component.Entities.Add(args.Used);
  45. Containers.Insert(args.Used, component.Container);
  46. // Not predicted so
  47. Audio.PlayPredicted(component.SoundInsert, uid, args.User);
  48. args.Handled = true;
  49. UpdateBallisticAppearance(uid, component);
  50. DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities));
  51. }
  52. private void OnBallisticAfterInteract(EntityUid uid, BallisticAmmoProviderComponent component, AfterInteractEvent args)
  53. {
  54. if (args.Handled ||
  55. !component.MayTransfer ||
  56. !Timing.IsFirstTimePredicted ||
  57. args.Target == null ||
  58. args.Used == args.Target ||
  59. Deleted(args.Target) ||
  60. !TryComp<BallisticAmmoProviderComponent>(args.Target, out var targetComponent) ||
  61. targetComponent.Whitelist == null)
  62. {
  63. return;
  64. }
  65. args.Handled = true;
  66. // Continuous loading
  67. _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.FillDelay, new AmmoFillDoAfterEvent(), used: uid, target: args.Target, eventTarget: uid)
  68. {
  69. BreakOnMove = true,
  70. BreakOnDamage = false,
  71. NeedHand = true,
  72. });
  73. }
  74. private void OnBallisticAmmoFillDoAfter(EntityUid uid, BallisticAmmoProviderComponent component, AmmoFillDoAfterEvent args)
  75. {
  76. if (args.Handled || args.Cancelled)
  77. return;
  78. if (Deleted(args.Target) ||
  79. !TryComp<BallisticAmmoProviderComponent>(args.Target, out var target) ||
  80. target.Whitelist == null)
  81. return;
  82. if (target.Entities.Count + target.UnspawnedCount == target.Capacity)
  83. {
  84. Popup(
  85. Loc.GetString("gun-ballistic-transfer-target-full",
  86. ("entity", args.Target)),
  87. args.Target,
  88. args.User);
  89. return;
  90. }
  91. if (component.Entities.Count + component.UnspawnedCount == 0)
  92. {
  93. Popup(
  94. Loc.GetString("gun-ballistic-transfer-empty",
  95. ("entity", uid)),
  96. uid,
  97. args.User);
  98. return;
  99. }
  100. void SimulateInsertAmmo(EntityUid ammo, EntityUid ammoProvider, EntityCoordinates coordinates)
  101. {
  102. // We call SharedInteractionSystem to raise contact events. Checks are already done by this point.
  103. _interaction.InteractUsing(args.User, ammo, ammoProvider, coordinates, checkCanInteract: false, checkCanUse: false);
  104. }
  105. List<(EntityUid? Entity, IShootable Shootable)> ammo = new();
  106. var evTakeAmmo = new TakeAmmoEvent(1, ammo, Transform(uid).Coordinates, args.User);
  107. RaiseLocalEvent(uid, evTakeAmmo);
  108. foreach (var (ent, _) in ammo)
  109. {
  110. if (ent == null)
  111. continue;
  112. if (_whitelistSystem.IsWhitelistFail(target.Whitelist, ent.Value))
  113. {
  114. Popup(
  115. Loc.GetString("gun-ballistic-transfer-invalid",
  116. ("ammoEntity", ent.Value),
  117. ("targetEntity", args.Target.Value)),
  118. uid,
  119. args.User);
  120. SimulateInsertAmmo(ent.Value, uid, Transform(uid).Coordinates);
  121. }
  122. else
  123. {
  124. // play sound to be cool
  125. Audio.PlayPredicted(component.SoundInsert, uid, args.User);
  126. SimulateInsertAmmo(ent.Value, args.Target.Value, Transform(args.Target.Value).Coordinates);
  127. }
  128. if (IsClientSide(ent.Value))
  129. Del(ent.Value);
  130. }
  131. // repeat if there is more space in the target and more ammo to fill it
  132. var moreSpace = target.Entities.Count + target.UnspawnedCount < target.Capacity;
  133. var moreAmmo = component.Entities.Count + component.UnspawnedCount > 0;
  134. args.Repeat = moreSpace && moreAmmo;
  135. }
  136. private void OnBallisticVerb(EntityUid uid, BallisticAmmoProviderComponent component, GetVerbsEvent<Verb> args)
  137. {
  138. if (!args.CanAccess || !args.CanInteract || args.Hands == null || !component.Cycleable)
  139. return;
  140. if (component.Cycleable)
  141. {
  142. args.Verbs.Add(new Verb()
  143. {
  144. Text = Loc.GetString("gun-ballistic-cycle"),
  145. Disabled = GetBallisticShots(component) == 0,
  146. Act = () => ManualCycle(uid, component, TransformSystem.GetMapCoordinates(uid), args.User),
  147. });
  148. }
  149. }
  150. private void OnBallisticExamine(EntityUid uid, BallisticAmmoProviderComponent component, ExaminedEvent args)
  151. {
  152. if (!args.IsInDetailsRange)
  153. return;
  154. args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", GetBallisticShots(component))));
  155. }
  156. private void ManualCycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates, EntityUid? user = null, GunComponent? gunComp = null)
  157. {
  158. if (!component.Cycleable)
  159. return;
  160. // Reset shotting for cycling
  161. if (Resolve(uid, ref gunComp, false) &&
  162. gunComp is { FireRateModified: > 0f } &&
  163. !Paused(uid))
  164. {
  165. gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRateModified);
  166. DirtyField(uid, gunComp, nameof(GunComponent.NextFire));
  167. }
  168. Audio.PlayPredicted(component.SoundRack, uid, user);
  169. var shots = GetBallisticShots(component);
  170. Cycle(uid, component, coordinates);
  171. var text = Loc.GetString(shots == 0 ? "gun-ballistic-cycled-empty" : "gun-ballistic-cycled");
  172. Popup(text, uid, user);
  173. UpdateBallisticAppearance(uid, component);
  174. UpdateAmmoCount(uid);
  175. }
  176. protected abstract void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates);
  177. private void OnBallisticInit(EntityUid uid, BallisticAmmoProviderComponent component, ComponentInit args)
  178. {
  179. component.Container = Containers.EnsureContainer<Container>(uid, "ballistic-ammo");
  180. // TODO: This is called twice though we need to support loading appearance data (and we need to call it on MapInit
  181. // to ensure it's correct).
  182. UpdateBallisticAppearance(uid, component);
  183. }
  184. private void OnBallisticMapInit(EntityUid uid, BallisticAmmoProviderComponent component, MapInitEvent args)
  185. {
  186. // TODO this should be part of the prototype, not set on map init.
  187. // Alternatively, just track spawned count, instead of unspawned count.
  188. if (component.Proto != null)
  189. {
  190. component.UnspawnedCount = Math.Max(0, component.Capacity - component.Container.ContainedEntities.Count);
  191. UpdateBallisticAppearance(uid, component);
  192. DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount));
  193. }
  194. }
  195. protected int GetBallisticShots(BallisticAmmoProviderComponent component)
  196. {
  197. return component.Entities.Count + component.UnspawnedCount;
  198. }
  199. private void OnBallisticTakeAmmo(EntityUid uid, BallisticAmmoProviderComponent component, TakeAmmoEvent args)
  200. {
  201. for (var i = 0; i < args.Shots; i++)
  202. {
  203. EntityUid entity;
  204. if (component.Entities.Count > 0)
  205. {
  206. entity = component.Entities[^1];
  207. args.Ammo.Add((entity, EnsureShootable(entity)));
  208. component.Entities.RemoveAt(component.Entities.Count - 1);
  209. DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities));
  210. Containers.Remove(entity, component.Container);
  211. }
  212. else if (component.UnspawnedCount > 0)
  213. {
  214. component.UnspawnedCount--;
  215. DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount));
  216. entity = Spawn(component.Proto, args.Coordinates);
  217. args.Ammo.Add((entity, EnsureShootable(entity)));
  218. }
  219. }
  220. UpdateBallisticAppearance(uid, component);
  221. }
  222. private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, ref GetAmmoCountEvent args)
  223. {
  224. args.Count = GetBallisticShots(component);
  225. args.Capacity = component.Capacity;
  226. }
  227. public void UpdateBallisticAppearance(EntityUid uid, BallisticAmmoProviderComponent component)
  228. {
  229. if (!Timing.IsFirstTimePredicted || !TryComp<AppearanceComponent>(uid, out var appearance))
  230. return;
  231. Appearance.SetData(uid, AmmoVisuals.AmmoCount, GetBallisticShots(component), appearance);
  232. Appearance.SetData(uid, AmmoVisuals.AmmoMax, component.Capacity, appearance);
  233. }
  234. public void SetBallisticUnspawned(Entity<BallisticAmmoProviderComponent> entity, int count)
  235. {
  236. if (entity.Comp.UnspawnedCount == count)
  237. return;
  238. entity.Comp.UnspawnedCount = count;
  239. UpdateBallisticAppearance(entity.Owner, entity.Comp);
  240. UpdateAmmoCount(entity.Owner);
  241. Dirty(entity);
  242. }
  243. }
  244. /// <summary>
  245. /// DoAfter event for filling one ballistic ammo provider from another.
  246. /// </summary>
  247. [Serializable, NetSerializable]
  248. public sealed partial class AmmoFillDoAfterEvent : SimpleDoAfterEvent
  249. {
  250. }