1
0

SharedGunSystem.Ballistic.cs 12 KB

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